mirror of https://github.com/apache/cassandra
Add format_bytes and format_time functions
patch by Stefan Miklosovic; reviewed by Jordan West for CASSANDRA-19546 Co-authored-by: Cheng Wang <chengw@netflix.com>
This commit is contained in:
parent
06f0965a08
commit
ba4a0d4fcb
|
|
@ -1,4 +1,5 @@
|
|||
5.1
|
||||
* Add format_bytes and format_time functions (CASSANDRA-19546)
|
||||
* Fix error when trying to assign a tuple to target type not being a tuple (CASSANDRA-20237)
|
||||
* Fail CREATE TABLE LIKE statement if UDTs in target keyspace do not exist or they have different structure from ones in source keyspace (CASSANDRA-19966)
|
||||
* Support octet_length and length functions (CASSANDRA-20102)
|
||||
|
|
|
|||
1
NEWS.txt
1
NEWS.txt
|
|
@ -113,6 +113,7 @@ New features
|
|||
the data and check the length themselves or enable UDF and register UDFs to check the length of columns.
|
||||
CASSANDRA-20102 adds a subset of the SQL99 (binary) string functions: "octet_length" defined on all types
|
||||
and "length" defined on UTF8 strings. See CASSANDRA-20102 for more information.
|
||||
- New functions `format_bytes` and `format_time` were added. See CASSANDRA-19546.
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
|
|
|||
|
|
@ -336,6 +336,189 @@ A number of functions allow to obtain the similarity score between vectors of fl
|
|||
|
||||
include::cassandra:partial$vector-search/vector_functions.adoc[]
|
||||
|
||||
[[human-helper-functions]]
|
||||
==== Human helper functions
|
||||
|
||||
For user's convenience, there are currently two functions which are converting values to more human-friendly represetations.
|
||||
|
||||
[cols=",,",options="header",]
|
||||
|===
|
||||
| Function name | Input type | Description
|
||||
| `format_bytes` |`int`, `tinyint`, `smallint`, `bigint`, `varint`, `ascii`, `text` | Converts values in bytes to a more human-friendly representation.
|
||||
|
||||
| `format_time` |`int`, `tinyint`, `smallint`, `bigint`, `varint`, `ascii`, `text` | Converts values in milliseconds to a more human-friendly representation.
|
||||
|
||||
|===
|
||||
|
||||
|
||||
===== format_bytes
|
||||
|
||||
This function looks at values in a column as if it was in bytes, and it will convert it to whatever a user pleases. Supported units are: `B`, `KiB`, `MiB` and `GiB`. The result will be rounded to two decimal places.
|
||||
|
||||
Supported column types on which this function is possible to be applied:
|
||||
`INT`, `TINYINT`, `SMALLINT`, `BIGINT`, `VARINT`, `ASCII`, `TEXT`.
|
||||
For `ASCII` and `TEXT` types, text of such column has to be a non-negative number.
|
||||
|
||||
Return values can be max of `Long.MAX_VALUE`, If the conversion produces overflown value, `Long.MAX_VALUE` will be returned.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The actual return value of the `Long.MAX_VALUE` will be 9223372036854776000 due to the limitations of floating-point precision.
|
||||
====
|
||||
|
||||
There are three ways how to call this function.
|
||||
Let's have this table:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
cqlsh> select * from ks.tb;
|
||||
|
||||
id | val
|
||||
----+----------------
|
||||
5 | 60000
|
||||
1 | 1234234
|
||||
2 | 12342341234234
|
||||
4 | 60001
|
||||
7 | null
|
||||
6 | 43
|
||||
3 | 123423
|
||||
|
||||
----
|
||||
|
||||
with schema
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE ks.tb (
|
||||
id int PRIMARY KEY,
|
||||
val bigint
|
||||
)
|
||||
----
|
||||
|
||||
Imagine that we wanted to look at `val` values as if they were in mebibytes. We would like to have more human-friendly output in order to not visually divide the values by 1024 in order to get them in respective bigger units. The following function call may take just a column itself as an argument, and it will
|
||||
automatically convert it.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The default source unit for `format_bytes` function is _bytes_, (`B`).
|
||||
====
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
cqlsh> select format_bytes(val) from ks.tb;
|
||||
|
||||
system.format_bytes(val)
|
||||
--------------------------
|
||||
58.59 KiB
|
||||
1.18 MiB
|
||||
11494.7 GiB
|
||||
58.59 KiB
|
||||
null
|
||||
43 B
|
||||
120.53 KiB
|
||||
----
|
||||
|
||||
The second way to call `format_bytes` functions is to specify into what size unit we would like to see all
|
||||
values to be converted to. For example, we want all size to be represented in mebibytes, hence we do:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
cqlsh> select format_bytes(val, 'MiB') from ks.tb;
|
||||
|
||||
system.format_bytes(val, 'MiB')
|
||||
----------------------------------
|
||||
0.06 MiB
|
||||
1.18 MiB
|
||||
11770573.84 MiB
|
||||
0.06 MiB
|
||||
null
|
||||
0 MiB
|
||||
0.12 MiB
|
||||
----
|
||||
|
||||
Lastly, we can specify a source unit and a target unit. A source unit tells what unit that column is logically of, the target unit tells what unit we want these values to be converted to. For example,
|
||||
if we know that our column is logically in kibibytes and we want them to be converted into mebibytes, we would do:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
cqlsh> select format_bytes(val, 'Kib', 'MiB') from ks.tb;
|
||||
|
||||
system.format_bytes(val, 'KiB', 'MiB')
|
||||
----------------------------------------
|
||||
58.59 MiB
|
||||
1205.31 MiB
|
||||
12053067611.56 MiB
|
||||
58.59 MiB
|
||||
null
|
||||
0.04 MiB
|
||||
120.53 MiB
|
||||
----
|
||||
|
||||
===== format_time
|
||||
|
||||
Similarly to `format_bytes`, we can do transformations on duration-like columns.
|
||||
|
||||
Supported units are: `d`, `h`, `m`, `s`, `ms`, `us`, `µs`, `ns`.
|
||||
|
||||
Supported column types on which this function is possible to be applied:
|
||||
`INT`, `TINYINT`, `SMALLINT`, `BIGINT`, `VARINT`, `ASCII`, `TEXT`. For `ASCII` and `TEXT` types, text of such column has to be a non-negative number.
|
||||
|
||||
Return values can be max of `Double.MAX_VALUE`, If the conversion produces overflown value, `Double.MAX_VALUE` will be returned.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The default source unit for `format_time` function is _milliseconds_, (`ms`).
|
||||
====
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
cqlsh> select format_time(val) from ks.tb;
|
||||
|
||||
system.format_time(val)
|
||||
-------------------------
|
||||
1 m
|
||||
20.57 m
|
||||
142851.17 d
|
||||
1 m
|
||||
null
|
||||
43 ms
|
||||
2.06 m
|
||||
----
|
||||
|
||||
We may specify what unit we want that value to be converted to, give the column's values are in millseconds:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
cqlsh> select format_time(val, 'm') from ks.tb;
|
||||
|
||||
system.format_time(val, 'm')
|
||||
------------------------------
|
||||
1 m
|
||||
20.57 m
|
||||
205705687.24 m
|
||||
1 m
|
||||
null
|
||||
0 m
|
||||
2.06 m
|
||||
----
|
||||
|
||||
Lastly, we can specify both source and target values:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
cqlsh> select format_time(val, 's', 'h') from ks.tb;
|
||||
|
||||
system.format_time(val, 's', 'h')
|
||||
-----------------------------------
|
||||
16.67 h
|
||||
342.84 h
|
||||
3428428120.62 h
|
||||
16.67 h
|
||||
null
|
||||
0.01 h
|
||||
34.28 h
|
||||
----
|
||||
|
||||
[[user-defined-scalar-functions]]
|
||||
=== User-defined functions
|
||||
|
||||
|
|
|
|||
|
|
@ -480,25 +480,21 @@ public abstract class DataStorageSpec
|
|||
{
|
||||
BYTES("B")
|
||||
{
|
||||
public long toBytes(long d)
|
||||
{
|
||||
return d;
|
||||
}
|
||||
public long toBytes(long d) { return d; }
|
||||
|
||||
public long toKibibytes(long d)
|
||||
{
|
||||
return (d / 1024L);
|
||||
}
|
||||
public long toKibibytes(long d) { return (d / 1024L); }
|
||||
|
||||
public long toMebibytes(long d)
|
||||
{
|
||||
return (d / (1024L * 1024));
|
||||
}
|
||||
public long toMebibytes(long d) { return (d / (1024L * 1024)); }
|
||||
|
||||
public long toGibibytes(long d)
|
||||
{
|
||||
return (d / (1024L * 1024 * 1024));
|
||||
}
|
||||
public long toGibibytes(long d) { return (d / (1024L * 1024 * 1024)); }
|
||||
|
||||
public double toBytesDouble(long d) { return (double) d; }
|
||||
|
||||
public double toKibibytesDouble(long d) { return d / 1024.0; }
|
||||
|
||||
public double toMebibytesDouble(long d) { return d / (1024.0 * 1024); }
|
||||
|
||||
public double toGibibytesDouble(long d) { return d / (1024.0 * 1024 * 1024); }
|
||||
|
||||
public long convert(long source, DataStorageUnit sourceUnit)
|
||||
{
|
||||
|
|
@ -527,6 +523,14 @@ public abstract class DataStorageSpec
|
|||
return (d / (1024L * 1024));
|
||||
}
|
||||
|
||||
public double toBytesDouble(long d) { return (double) toBytes(d); }
|
||||
|
||||
public double toKibibytesDouble(long d) { return (double) d; }
|
||||
|
||||
public double toMebibytesDouble(long d) { return d / 1024.0; }
|
||||
|
||||
public double toGibibytesDouble(long d) { return d / (1024.0 * 1024); }
|
||||
|
||||
public long convert(long source, DataStorageUnit sourceUnit)
|
||||
{
|
||||
return sourceUnit.toKibibytes(source);
|
||||
|
|
@ -544,16 +548,21 @@ public abstract class DataStorageSpec
|
|||
return x(d, 1024L, (MAX / 1024L));
|
||||
}
|
||||
|
||||
public long toMebibytes(long d)
|
||||
{
|
||||
return d;
|
||||
}
|
||||
public long toMebibytes(long d) { return d; }
|
||||
|
||||
public long toGibibytes(long d)
|
||||
{
|
||||
return (d / 1024L);
|
||||
}
|
||||
|
||||
public double toBytesDouble(long d) { return (double) toBytes(d); }
|
||||
|
||||
public double toKibibytesDouble(long d) { return (double) toKibibytes(d); }
|
||||
|
||||
public double toMebibytesDouble(long d) { return (double) d; }
|
||||
|
||||
public double toGibibytesDouble(long d) { return d / 1024.0; }
|
||||
|
||||
public long convert(long source, DataStorageUnit sourceUnit)
|
||||
{
|
||||
return sourceUnit.toMebibytes(source);
|
||||
|
|
@ -576,10 +585,15 @@ public abstract class DataStorageSpec
|
|||
return x(d, 1024L, (MAX / 1024L));
|
||||
}
|
||||
|
||||
public long toGibibytes(long d)
|
||||
{
|
||||
return d;
|
||||
}
|
||||
public long toGibibytes(long d) { return d; }
|
||||
|
||||
public double toBytesDouble(long d) { return (double) toBytes(d); }
|
||||
|
||||
public double toKibibytesDouble(long d) { return (double) toKibibytes(d); }
|
||||
|
||||
public double toMebibytesDouble(long d) { return (double) toMebibytes(d); }
|
||||
|
||||
public double toGibibytesDouble(long d) { return (double) d; }
|
||||
|
||||
public long convert(long source, DataStorageUnit sourceUnit)
|
||||
{
|
||||
|
|
@ -628,26 +642,51 @@ public abstract class DataStorageSpec
|
|||
this.symbol = symbol;
|
||||
}
|
||||
|
||||
public String getSymbol()
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
|
||||
public long toBytes(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public double toBytesDouble(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public long toKibibytes(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public double toKibibytesDouble(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public long toMebibytes(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public double toMebibytesDouble(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public long toGibibytes(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public double toGibibytesDouble(long d)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
|
||||
public long convert(long source, DataStorageUnit sourceUnit)
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ public abstract class DurationSpec
|
|||
* @param symbol the time unit symbol
|
||||
* @return the time unit associated to the specified symbol
|
||||
*/
|
||||
static TimeUnit fromSymbol(String symbol)
|
||||
public static TimeUnit fromSymbol(String symbol)
|
||||
{
|
||||
switch (toLowerCaseLocalized(symbol))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,447 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.cql3.functions;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.cassandra.config.DataStorageSpec.DataStorageUnit;
|
||||
import org.apache.cassandra.config.DurationSpec;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.BYTES;
|
||||
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.GIBIBYTES;
|
||||
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.KIBIBYTES;
|
||||
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.MEBIBYTES;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.ASCII;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.BIGINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.INT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.SMALLINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.TEXT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.TINYINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.VARINT;
|
||||
import static org.apache.cassandra.cql3.functions.FunctionParameter.fixed;
|
||||
import static org.apache.cassandra.cql3.functions.FunctionParameter.optional;
|
||||
|
||||
public class FormatFcts
|
||||
{
|
||||
private static final DecimalFormat decimalFormat;
|
||||
|
||||
static
|
||||
{
|
||||
decimalFormat = new DecimalFormat("#.##");
|
||||
decimalFormat.setRoundingMode(RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a double value to a string with two decimal places.
|
||||
* <p>
|
||||
* Supported column types on which this function is possible to be applied:
|
||||
* <pre>DOUBLE</pre>
|
||||
*/
|
||||
public static String format(double value)
|
||||
{
|
||||
return decimalFormat.format(value);
|
||||
}
|
||||
|
||||
public static void addFunctionsTo(NativeFunctions functions)
|
||||
{
|
||||
functions.add(FormatBytesFct.factory());
|
||||
functions.add(FormatTimeFct.factory());
|
||||
}
|
||||
|
||||
private static long validateAndGetValue(Arguments arguments)
|
||||
{
|
||||
if (arguments.containsNulls())
|
||||
throw new InvalidRequestException("none of the arguments may be null");
|
||||
|
||||
long value = getValue(arguments);
|
||||
|
||||
if (value < 0)
|
||||
throw new InvalidRequestException("value must be non-negative");
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static long getValue(Arguments arguments)
|
||||
{
|
||||
Optional<String> maybeString = getAsString(arguments, 0);
|
||||
|
||||
if (maybeString.isPresent())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Long.parseLong(maybeString.get());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidRequestException("unable to convert string '" + maybeString.get() + "' to a value of type long");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return arguments.getAsLong(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> getAsString(Arguments arguments, int i)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Optional.ofNullable(arguments.get(i));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private FormatFcts()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts numeric value in a column to a value of specified unit.
|
||||
* <p>
|
||||
* If the function call contains just one argument - value to convert - then it will be
|
||||
* looked at as the value is of unit 'ms' and it will be converted to a value of a unit which is closest to it.
|
||||
* The result will be rounded to two decimal places.
|
||||
* E.g. If a value is (20 * 1000 + 250) then the unit will be in seconds and converted value will be 20.25.
|
||||
* <p>
|
||||
* If the function call contains two arguments - value to convert and a unit - then it will be looked at
|
||||
* as the unit of such value is 'ms' and it will be converted into the value of the second (unit) argument.
|
||||
* <p>
|
||||
* If the function call contains three arguments - value to covert and source and target unit - then the value
|
||||
* will be considered of a unit of the second argument, and it will be converted
|
||||
* into a value of the third (unit) argument.
|
||||
* <p>
|
||||
* Examples:
|
||||
* <pre>
|
||||
* format_time(val)
|
||||
* format_time(val, 'm') = format_time(val, 'ms', 'm')
|
||||
* format_time(val, 's', 'm')
|
||||
* format_time(val, 's', 'h')
|
||||
* format_time(val, 's', 'd')
|
||||
* format_time(val, 's') = format_time(val, 'ms', 's')
|
||||
* format_time(val, 'h') = format_time(val, 'ms', 'h')
|
||||
* </pre>
|
||||
* <p>
|
||||
* It is possible to convert values of a bigger unit to values of a smaller unit, e.g. this is possible:
|
||||
*
|
||||
* <pre>
|
||||
* format_time(val, 'm', 's')
|
||||
* </pre>
|
||||
* <p>
|
||||
* Values can be max of Double.MAX_VALUE, If the conversion produces overflown value, Double.MAX_VALUE will be returned.
|
||||
* <p>
|
||||
* Supported units are: d, h, m, s, ms, us, µs, ns
|
||||
* <p>
|
||||
* Supported column types on which this function is possible to be applied:
|
||||
* <pre>INT, TINYINT, SMALLINT, BIGINT, VARINT, ASCII, TEXT</pre>
|
||||
* For ASCII and TEXT types, text of such column has to be a non-negative number.
|
||||
* <p>
|
||||
* The conversion of negative values is not supported.
|
||||
*/
|
||||
public static class FormatTimeFct extends NativeScalarFunction
|
||||
{
|
||||
private static final String FUNCTION_NAME = "format_time";
|
||||
|
||||
private static final String[] UNITS = { "d", "h", "m", "s" };
|
||||
private static final long[] CONVERSION_FACTORS = { 86400000, 3600000, 60000, 1000 }; // Milliseconds in a day, hour, minute, second
|
||||
|
||||
private FormatTimeFct(AbstractType<?>... argsTypes)
|
||||
{
|
||||
super(FUNCTION_NAME, UTF8Type.instance, argsTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer execute(Arguments arguments) throws InvalidRequestException
|
||||
{
|
||||
if (arguments.get(0) == null)
|
||||
return null;
|
||||
|
||||
long value = validateAndGetValue(arguments);
|
||||
|
||||
if (arguments.size() == 1)
|
||||
{
|
||||
Pair<Double, String> convertedValue = convertValue(value);
|
||||
return UTF8Type.instance.fromString(format(convertedValue.left) + ' ' + convertedValue.right);
|
||||
}
|
||||
|
||||
TimeUnit sourceUnit;
|
||||
TimeUnit targetUnit;
|
||||
String targetUnitAsString;
|
||||
|
||||
if (arguments.size() == 2)
|
||||
{
|
||||
sourceUnit = MILLISECONDS;
|
||||
targetUnitAsString = arguments.get(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceUnit = validateUnit(arguments.get(1));
|
||||
targetUnitAsString = arguments.get(2);
|
||||
}
|
||||
|
||||
targetUnit = validateUnit(targetUnitAsString);
|
||||
|
||||
double convertedValue = convertValue(value, sourceUnit, targetUnit);
|
||||
return UTF8Type.instance.fromString(format(convertedValue) + ' ' + targetUnitAsString);
|
||||
}
|
||||
|
||||
private TimeUnit validateUnit(String unitAsString)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DurationSpec.fromSymbol(unitAsString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidRequestException(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Pair<Double, String> convertValue(long valueToConvert)
|
||||
{
|
||||
for (int i = 0; i < CONVERSION_FACTORS.length; i++)
|
||||
{
|
||||
if (valueToConvert >= CONVERSION_FACTORS[i])
|
||||
{
|
||||
double convertedValue = (double) valueToConvert / CONVERSION_FACTORS[i];
|
||||
return Pair.create(convertedValue, UNITS[i]);
|
||||
}
|
||||
}
|
||||
return Pair.create((double) valueToConvert, "ms");
|
||||
}
|
||||
|
||||
private Double convertValue(long valueToConvert, TimeUnit sourceUnit, TimeUnit targetUnit)
|
||||
{
|
||||
try
|
||||
{
|
||||
double conversionFactor = getConversionFactor(sourceUnit, targetUnit);
|
||||
return valueToConvert * conversionFactor;
|
||||
}
|
||||
catch (ArithmeticException ex)
|
||||
{
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
private static double getConversionFactor(TimeUnit sourceUnit, TimeUnit targetUnit)
|
||||
{
|
||||
// Define conversion factors between units
|
||||
double nanosPerSourceUnit = getNanosPerUnit(sourceUnit);
|
||||
double nanosPerTargetUnit = getNanosPerUnit(targetUnit);
|
||||
|
||||
// Calculate the conversion factor
|
||||
return nanosPerSourceUnit / nanosPerTargetUnit;
|
||||
}
|
||||
|
||||
private static double getNanosPerUnit(TimeUnit unit)
|
||||
{
|
||||
switch (unit)
|
||||
{
|
||||
case NANOSECONDS:
|
||||
return 1.0;
|
||||
case MICROSECONDS:
|
||||
return 1_000.0;
|
||||
case MILLISECONDS:
|
||||
return 1_000_000.0;
|
||||
case SECONDS:
|
||||
return 1_000_000_000.0;
|
||||
case MINUTES:
|
||||
return 60.0 * 1_000_000_000.0;
|
||||
case HOURS:
|
||||
return 3600.0 * 1_000_000_000.0;
|
||||
case DAYS:
|
||||
return 86400.0 * 1_000_000_000.0;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported time unit: " + unit);
|
||||
}
|
||||
}
|
||||
|
||||
public static FunctionFactory factory()
|
||||
{
|
||||
return new FunctionFactory(FUNCTION_NAME,
|
||||
fixed(INT, TINYINT, SMALLINT, BIGINT, VARINT, ASCII, TEXT),
|
||||
optional(fixed(ASCII)),
|
||||
optional(fixed(ASCII)))
|
||||
{
|
||||
@Override
|
||||
protected NativeFunction doGetOrCreateFunction(List<AbstractType<?>> argTypes, AbstractType<?> receiverType)
|
||||
{
|
||||
if (argTypes.isEmpty() || argTypes.size() > 3)
|
||||
throw invalidNumberOfArgumentsException();
|
||||
|
||||
return new FormatTimeFct(argTypes.toArray(new AbstractType<?>[0]));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts numeric value in a column to a size value of specified unit.
|
||||
* <p>
|
||||
* If the function call contains just one argument - value to convert - then it will be
|
||||
* looked at as the value is of unit 'B' and it will be converted to a value of a unit which is closest to it.
|
||||
* The result will be rounded to two decimal places.
|
||||
* E.g. If a value is (100 * 1024 + 150) then the unit will be in KiB and converted value will be 100.15.
|
||||
* <p>
|
||||
* If the function call contains two arguments - value to convert and a unit - then it will be looked at
|
||||
* as the unit of such value is 'B' and it will be converted into the value of the second (unit) argument.
|
||||
* <p>
|
||||
* If the function call contains three arguments - value to covert and source and target unit - then the value
|
||||
* will be considered of a unit of the second argument, and it will be converted
|
||||
* into a value of the third (unit) argument.
|
||||
* <p>
|
||||
* Examples:
|
||||
* <pre>
|
||||
* format_bytes(val) = format_bytes(val, 'B', 'MiB')
|
||||
* format_bytes(val, 'B', 'MiB')
|
||||
* format_bytes(val, 'B', 'GiB')
|
||||
* format_bytes(val, 'KiB', 'GiB')
|
||||
* format_bytes(val, 'MiB') = format_bytes(val, 'B', 'MiB')
|
||||
* format_bytes(val, 'GiB') = format_bytes(val, 'B', 'GiB')
|
||||
* </pre>
|
||||
* <p>
|
||||
* It is possible to convert values of a bigger unit to values of a smaller unit, e.g. this is possible:
|
||||
*
|
||||
* <pre>
|
||||
* format_bytes(val, 'GiB', 'B')
|
||||
* </pre>
|
||||
* <p>
|
||||
* Values can be max of Long.MAX_VALUE, If the conversion produces overflown value, Long.MAX_VALUE will be returned.
|
||||
* Note that the actual return value will be 9223372036854776000 due to the limitations of double precision.
|
||||
* <p>
|
||||
* Supported units are: B, KiB, MiB, GiB
|
||||
* <p>
|
||||
* Supported column types on which this function is possible to be applied:
|
||||
* <pre>INT, TINYINT, SMALLINT, BIGINT, VARINT, ASCII, TEXT</pre>
|
||||
* For ASCII and TEXT types, text of such column has to be a non-negative number.
|
||||
* <p>
|
||||
* <p>
|
||||
* The conversion of negative values is not supported.
|
||||
*/
|
||||
public static class FormatBytesFct extends NativeScalarFunction
|
||||
{
|
||||
private static final String FUNCTION_NAME = "format_bytes";
|
||||
|
||||
private FormatBytesFct(AbstractType<?>... argsTypes)
|
||||
{
|
||||
super(FUNCTION_NAME, UTF8Type.instance, argsTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer execute(Arguments arguments) throws InvalidRequestException
|
||||
{
|
||||
if (arguments.get(0) == null)
|
||||
return null;
|
||||
|
||||
long value = validateAndGetValue(arguments);
|
||||
|
||||
DataStorageUnit sourceUnit;
|
||||
DataStorageUnit targetUnit;
|
||||
|
||||
if (arguments.size() == 1)
|
||||
{
|
||||
sourceUnit = BYTES;
|
||||
|
||||
if (value > FileUtils.ONE_GIB)
|
||||
targetUnit = GIBIBYTES;
|
||||
else if (value > FileUtils.ONE_MIB)
|
||||
targetUnit = MEBIBYTES;
|
||||
else if (value > FileUtils.ONE_KIB)
|
||||
targetUnit = KIBIBYTES;
|
||||
else
|
||||
targetUnit = BYTES;
|
||||
}
|
||||
else if (arguments.size() == 2)
|
||||
{
|
||||
sourceUnit = BYTES;
|
||||
targetUnit = validateUnit(arguments.get(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceUnit = validateUnit(arguments.get(1));
|
||||
targetUnit = validateUnit(arguments.get(2));
|
||||
}
|
||||
|
||||
double convertedValue = convertValue(value, sourceUnit, targetUnit);
|
||||
String convertedValueAsString = format(convertedValue);
|
||||
|
||||
return UTF8Type.instance.fromString(convertedValueAsString + ' ' + targetUnit.getSymbol());
|
||||
}
|
||||
|
||||
private double convertValue(long valueToConvert, DataStorageUnit sourceUnit, DataStorageUnit targetUnit)
|
||||
{
|
||||
switch (targetUnit)
|
||||
{
|
||||
case BYTES:
|
||||
return sourceUnit.toBytesDouble(valueToConvert);
|
||||
case KIBIBYTES:
|
||||
return sourceUnit.toKibibytesDouble(valueToConvert);
|
||||
case MEBIBYTES:
|
||||
return sourceUnit.toMebibytesDouble(valueToConvert);
|
||||
case GIBIBYTES:
|
||||
return sourceUnit.toGibibytesDouble(valueToConvert);
|
||||
default:
|
||||
throw new InvalidRequestException("unsupported target unit " + targetUnit);
|
||||
}
|
||||
}
|
||||
|
||||
private DataStorageUnit validateUnit(String unitAsString)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DataStorageUnit.fromSymbol(unitAsString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidRequestException(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static FunctionFactory factory()
|
||||
{
|
||||
return new FunctionFactory(FUNCTION_NAME,
|
||||
fixed(INT, TINYINT, SMALLINT, BIGINT, VARINT, ASCII, TEXT),
|
||||
optional(fixed(ASCII)),
|
||||
optional(fixed(ASCII)))
|
||||
{
|
||||
@Override
|
||||
protected NativeFunction doGetOrCreateFunction(List<AbstractType<?>> argTypes, AbstractType<?> receiverType)
|
||||
{
|
||||
if (argTypes.isEmpty() || argTypes.size() > 3)
|
||||
throw invalidNumberOfArgumentsException();
|
||||
|
||||
return new FormatBytesFct(argTypes.toArray(new AbstractType<?>[0]));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ public class NativeFunctions
|
|||
MaskingFcts.addFunctionsTo(this);
|
||||
VectorFcts.addFunctionsTo(this);
|
||||
ClusterMetadataFcts.addFunctionsTo(this);
|
||||
FormatFcts.addFunctionsTo(this);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.cql3.functions;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Ignore;
|
||||
|
||||
import org.apache.cassandra.cql3.CQL3Type;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
|
||||
@Ignore
|
||||
public abstract class AbstractFormatFctTest extends CQLTester
|
||||
{
|
||||
protected void createTable(List<CQL3Type.Native> columnTypes, Object[][] rows)
|
||||
{
|
||||
String[][] columns = new String[columnTypes.size() + 1][2];
|
||||
|
||||
columns[0][0] = "pk";
|
||||
columns[0][1] = "int";
|
||||
|
||||
for (int i = 1; i <= columnTypes.size(); i++)
|
||||
{
|
||||
columns[i][0] = "col" + i;
|
||||
columns[i][1] = columnTypes.get(i - 1).name().toLowerCase();
|
||||
}
|
||||
|
||||
createTable(columns, rows);
|
||||
}
|
||||
|
||||
protected void createDefaultTable(Object[][] rows)
|
||||
{
|
||||
createTable(new String[][]{ { "pk", "int" }, { "col1", "int" }, { "col2", "int" } }, rows);
|
||||
}
|
||||
|
||||
protected void createTable(String[][] columns, Object[][] rows)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < columns.length; i++)
|
||||
{
|
||||
sb.append(columns[i][0]);
|
||||
sb.append(' ');
|
||||
sb.append(columns[i][1]);
|
||||
|
||||
if (i == 0)
|
||||
sb.append(" primary key");
|
||||
|
||||
if (i + 1 != columns.length)
|
||||
sb.append(", ");
|
||||
}
|
||||
String columnsDefinition = sb.toString();
|
||||
createTable(KEYSPACE, "CREATE TABLE %s (" + columnsDefinition + ')');
|
||||
|
||||
String cols = Arrays.stream(columns).map(s -> s[0]).collect(Collectors.joining(", "));
|
||||
|
||||
for (Object[] row : rows)
|
||||
{
|
||||
String vals = Arrays.stream(row).map(v -> {
|
||||
if (v == null)
|
||||
return "null";
|
||||
return v.toString();
|
||||
}).collect(Collectors.joining(", "));
|
||||
execute("INSERT INTO %s (" + cols + ") values (" + vals + ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.cql3.functions;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
|
||||
import static java.util.List.of;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.ASCII;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.BIGINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.INT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.SMALLINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.TEXT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.TINYINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.VARINT;
|
||||
import static org.apache.cassandra.cql3.functions.FormatFcts.format;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.quicktheories.QuickTheory.qt;
|
||||
import static org.quicktheories.generators.SourceDSL.integers;
|
||||
|
||||
public class FormatBytesFctTest extends AbstractFormatFctTest
|
||||
{
|
||||
@Test
|
||||
public void testOneValueArgumentExact()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1073741825 },
|
||||
{ 2, 1073741823 },
|
||||
{ 3, 0 } }); // 0 B
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("1 GiB"));
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 2"), row("1024 MiB"));
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 3"), row("0 B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneValueArgumentDecimalRoundup()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1563401650 },
|
||||
{ 2, 1072441589 },
|
||||
{ 3, 102775 },
|
||||
{ 4, 102 } });
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("1.46 GiB")); // 1.4560
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 2"), row("1022.76 MiB")); // 1022.7599
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 3"), row("100.37 KiB")); // 100.3662
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 4"), row("102 B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneValueArgumentDecimalRoundDown()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1557999386 },
|
||||
{ 2, 1072433201 },
|
||||
{ 3, 102769 },
|
||||
{ 4, 102 } });
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("1.45 GiB")); // 1.451
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 2"), row("1022.75 MiB")); // 1022.752
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 3"), row("100.36 KiB")); // 100.3613
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 4"), row("102 B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueAndUnitArgumentsExact()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1073741825 },
|
||||
{ 2, 0 } });
|
||||
assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 1"), row("1073741825 B"));
|
||||
assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 1"), row("1048576 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 1"), row("1024 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 1"), row("1 GiB"));
|
||||
|
||||
assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 2"), row("0 B"));
|
||||
assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 2"), row("0 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 2"), row("0 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 2"), row("0 GiB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueAndUnitArgumentsDecimal()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1563401650 },
|
||||
{ 2, 1557999336 } });
|
||||
assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 1"), row("1563401650 B"));
|
||||
assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 1"), row("1526759.42 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 1"), row("1490.98 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 1"), row("1.46 GiB"));
|
||||
|
||||
assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 2"), row("1557999336 B"));
|
||||
assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 2"), row("1521483.73 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 2"), row("1485.82 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 2"), row("1.45 GiB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueWithSourceAndTargetArgumentExact()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1073741825 },
|
||||
{ 2, 1 },
|
||||
{ 3, 0 } });
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'B') from %s where pk = 1"), row("1073741825 B"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 1"), row("1048576 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'MiB') from %s where pk = 1"), row("1024 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 1"), row("1 GiB"));
|
||||
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'GiB') from %s where pk = 2"), row("1 GiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'MiB') from %s where pk = 2"), row("1024 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'KiB') from %s where pk = 2"), row("1048576 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'B') from %s where pk = 2"), row("1073741824 B"));
|
||||
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'GiB') from %s where pk = 3"), row("0 GiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'MiB') from %s where pk = 3"), row("0 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'KiB') from %s where pk = 3"), row("0 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'B') from %s where pk = 3"), row("0 B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueWithSourceAndTargetArgumentDecimal()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1563401650 },
|
||||
{ 2, 1557999336 },});
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'B') from %s where pk = 1"), row("1563401650 B"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 1"), row("1526759.42 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'MiB') from %s where pk = 1"), row("1490.98 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 1"), row("1.46 GiB"));
|
||||
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'B') from %s where pk = 2"), row("1557999336 B"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 2"), row("1521483.73 KiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'MiB') from %s where pk = 2"), row("1485.82 MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 2"), row("1.45 GiB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFuzzNumberGenerators()
|
||||
{
|
||||
createTable("CREATE TABLE %s (pk int primary key, col1 int)");
|
||||
|
||||
qt().withExamples(1024).forAll(integers().allPositive()).checkAssert(
|
||||
(randInt) -> {
|
||||
execute("INSERT INTO %s (pk, col1) VALUES (?, ?)", 1, randInt);
|
||||
|
||||
assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 1"), row(format(randInt / 1024.0 / 1024.0) + " MiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'KiB', 'GiB') from %s where pk = 1"), row(format(randInt / 1024.0 / 1024.0) + " GiB"));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 1"), row(format(randInt / 1024.0 / 1024.0 / 1024.0 ) + " GiB"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverflow()
|
||||
{
|
||||
createTable(of(BIGINT, INT, SMALLINT, TINYINT),
|
||||
new Object[][]{ { 1,
|
||||
1073741825L * 1024 + 1,
|
||||
Integer.MAX_VALUE - 1,
|
||||
Short.MAX_VALUE - 1,
|
||||
Byte.MAX_VALUE - 1 },
|
||||
{ 2,
|
||||
1073741825L * 1024 + 1,
|
||||
Integer.MAX_VALUE,
|
||||
Short.MAX_VALUE,
|
||||
Byte.MAX_VALUE } });
|
||||
|
||||
// this will stop at Long.MAX_VALUE
|
||||
assertRows(execute("select format_bytes(col1, 'GiB', 'B') from %s where pk = 1"), row("9223372036854776000 B"));
|
||||
assertRows(execute("select format_bytes(col2, 'GiB', 'B') from %s where pk = 1"), row("2305843007066210300 B"));
|
||||
assertRows(execute("select format_bytes(col3, 'GiB', 'B') from %s where pk = 1"), row("35182224605184 B"));
|
||||
assertRows(execute("select format_bytes(col4, 'GiB', 'B') from %s where pk = 1"), row("135291469824 B"));
|
||||
|
||||
assertRows(execute("select format_bytes(col2, 'GiB', 'B') from %s where pk = 2"), row("2305843008139952130 B"));
|
||||
assertRows(execute("select format_bytes(col3, 'GiB', 'B') from %s where pk = 2"), row("35183298347008 B"));
|
||||
assertRows(execute("select format_bytes(col4, 'GiB', 'B') from %s where pk = 2"), row("136365211648 B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllSupportedColumnTypes()
|
||||
{
|
||||
createTable(of(INT, TINYINT, SMALLINT, BIGINT, VARINT, ASCII, TEXT),
|
||||
new Object[][]{ { 1,
|
||||
Integer.MAX_VALUE,
|
||||
Byte.MAX_VALUE,
|
||||
Short.MAX_VALUE,
|
||||
Long.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
'\'' + Integer.valueOf(Integer.MAX_VALUE).toString() + '\'',
|
||||
'\'' + Integer.valueOf(Integer.MAX_VALUE).toString() + '\'',
|
||||
} });
|
||||
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("2 GiB"));
|
||||
assertRows(execute("select format_bytes(col2) from %s where pk = 1"), row("127 B"));
|
||||
assertRows(execute("select format_bytes(col3) from %s where pk = 1"), row("32 KiB"));
|
||||
assertRows(execute("select format_bytes(col4) from %s where pk = 1"), row("8589934592 GiB"));
|
||||
assertRows(execute("select format_bytes(col5) from %s where pk = 1"), row("2 GiB"));
|
||||
assertRows(execute("select format_bytes(col6) from %s where pk = 1"), row("2 GiB"));
|
||||
assertRows(execute("select format_bytes(col7) from %s where pk = 1"), row("2 GiB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeValueIsInvalid()
|
||||
{
|
||||
createDefaultTable(new Object[][]{ { "1", "-1", "-2" } });
|
||||
assertThatThrownBy(() -> execute("select format_bytes(col1) from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("value must be non-negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnparsableTextIsInvalid()
|
||||
{
|
||||
createTable(of(TEXT), new Object[][]{ { 1, "'abc'" }, { 2, "'-1'" } });
|
||||
|
||||
assertThatThrownBy(() -> execute("select format_bytes(col1) from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("unable to convert string 'abc' to a value of type long");
|
||||
|
||||
assertThatThrownBy(() -> execute("select format_bytes(col1) from %s where pk = 2"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("value must be non-negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidUnits()
|
||||
{
|
||||
createDefaultTable(new Object[][]{ { "1", "1", "2" } });
|
||||
for (String functionCall : new String[] {
|
||||
"format_bytes(col1, 'abc')",
|
||||
"format_bytes(col1, 'B', 'abc')",
|
||||
"format_bytes(col1, 'abc', 'B')",
|
||||
"format_bytes(col1, 'abc', 'abc')"
|
||||
})
|
||||
{
|
||||
assertThatThrownBy(() -> execute("select " + functionCall + " from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("Unsupported data storage unit: abc. Supported units are: B, KiB, MiB, GiB");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidArgumentsSize()
|
||||
{
|
||||
createDefaultTable(new Object[][]{ { "1", "1", "2" } });
|
||||
|
||||
// Test arguemnt size = 0
|
||||
assertThatThrownBy(() -> execute("select format_bytes() from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("Invalid number of arguments for function system.format_bytes([int|tinyint|smallint|bigint|varint|ascii|text], [ascii], [ascii])");
|
||||
|
||||
// Test argument size > 3
|
||||
assertThatThrownBy(() -> execute("select format_bytes(col1, 'B', 'KiB', 'GiB') from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("Invalid number of arguments for function system.format_bytes([int|tinyint|smallint|bigint|varint|ascii|text], [ascii], [ascii])");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlingNullValues()
|
||||
{
|
||||
createTable(of(TEXT, ASCII, INT),
|
||||
new Object[][]{ { 1, null, null, null } });
|
||||
|
||||
assertRows(execute("select format_bytes(col1), format_bytes(col2), format_bytes(col3) from %s where pk = 1"),
|
||||
row(null, null, null));
|
||||
|
||||
assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 1"), row((Object) null));
|
||||
assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 1"), row((Object) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlingNullArguments()
|
||||
{
|
||||
createTable(of(TEXT, ASCII, INT),
|
||||
new Object[][]{ { 1, null, null, null },
|
||||
{ 2, "'1'", "'2'", 3 } });
|
||||
|
||||
assertRows(execute("select format_bytes(col1, null) from %s where pk = 1"), row((Object) null));
|
||||
|
||||
for (String functionCall : new String[] {
|
||||
"format_bytes(col3, null)",
|
||||
"format_bytes(col3, null, null)",
|
||||
"format_bytes(col3, null, 'KiB')",
|
||||
"format_bytes(col3, 'KiB', null)"
|
||||
})
|
||||
{
|
||||
assertThatThrownBy(() -> execute("select " + functionCall + " from %s where pk = 2"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("none of the arguments may be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeSmallerThan1KibiByte()
|
||||
{
|
||||
createDefaultTable(new Object[][]{ { "1", "900", "2000" } });
|
||||
assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("900 B"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.cql3.functions;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
|
||||
import static java.util.List.of;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.ASCII;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.BIGINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.INT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.SMALLINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.TEXT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.TINYINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.VARINT;
|
||||
import static org.apache.cassandra.cql3.functions.FormatFcts.format;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import org.quicktheories.WithQuickTheories;
|
||||
|
||||
public class FormatTimeFctTest extends AbstractFormatFctTest implements WithQuickTheories
|
||||
{
|
||||
@Test
|
||||
public void testOneValueArgument()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 7200001 }, // 2h + 1ms
|
||||
{ 2, 7199999 }, // 2h - 1ms
|
||||
{ 3, 0 } }); // 0 B
|
||||
assertRows(execute("select format_time(col1) from %s where pk = 1"), row("2 h"));
|
||||
assertRows(execute("select format_time(col1) from %s where pk = 2"), row("2 h"));
|
||||
assertRows(execute("select format_time(col1) from %s where pk = 3"), row("0 ms"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneValueArgumentDecimal()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 9000000 }, // 2.5h
|
||||
{ 2, 7704000 }, // 2.14h
|
||||
{ 3, 7848000 } }); // 2.18h
|
||||
assertRows(execute("select format_time(col1) from %s where pk = 1"), row("2.5 h"));
|
||||
assertRows(execute("select format_time(col1) from %s where pk = 2"), row("2.14 h"));
|
||||
assertRows(execute("select format_time(col1) from %s where pk = 3"), row("2.18 h"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueAndUnitArguments()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1073741826 },
|
||||
{ 2, 0 }});
|
||||
assertRows(execute("select format_time(col1, 's') from %s where pk = 1"), row("1073741.83 s"));
|
||||
assertRows(execute("select format_time(col1, 'm') from %s where pk = 1"), row("17895.7 m"));
|
||||
assertRows(execute("select format_time(col1, 'h') from %s where pk = 1"), row("298.26 h"));
|
||||
assertRows(execute("select format_time(col1, 'd') from %s where pk = 1"), row("12.43 d"));
|
||||
|
||||
assertRows(execute("select format_time(col1, 's') from %s where pk = 2"), row("0 s"));
|
||||
assertRows(execute("select format_time(col1, 'm') from %s where pk = 2"), row("0 m"));
|
||||
assertRows(execute("select format_time(col1, 'h') from %s where pk = 2"), row("0 h"));
|
||||
assertRows(execute("select format_time(col1, 'd') from %s where pk = 2"), row("0 d"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueWithSourceAndTargetArgument()
|
||||
{
|
||||
createTable(of(INT), new Object[][]{ { 1, 1073741826 },
|
||||
{ 2, 1 },
|
||||
{ 3, 0 } });
|
||||
assertRows(execute("select format_time(col1, 'ns', 'us') from %s where pk = 1"), row("1073741.83 us"));
|
||||
assertRows(execute("select format_time(col1, 'ns', 'ms') from %s where pk = 1"), row("1073.74 ms"));
|
||||
assertRows(execute("select format_time(col1, 'ns', 's') from %s where pk = 1"), row("1.07 s"));
|
||||
assertRows(execute("select format_time(col1, 'ns', 'm') from %s where pk = 1"), row("0.02 m"));
|
||||
|
||||
assertRows(execute("select format_time(col1, 'us', 'ns') from %s where pk = 1"), row("1073741826000 ns"));
|
||||
assertRows(execute("select format_time(col1, 'us', 'ms') from %s where pk = 1"), row("1073741.83 ms"));
|
||||
assertRows(execute("select format_time(col1, 'us', 's') from %s where pk = 1"), row("1073.74 s"));
|
||||
assertRows(execute("select format_time(col1, 'us', 'm') from %s where pk = 1"), row("17.9 m"));
|
||||
assertRows(execute("select format_time(col1, 'us', 'h') from %s where pk = 1"), row("0.3 h"));
|
||||
assertRows(execute("select format_time(col1, 'us', 'd') from %s where pk = 1"), row("0.01 d"));
|
||||
|
||||
assertRows(execute("select format_time(col1, 'ms', 'ms') from %s where pk = 1"), row("1073741826 ms"));
|
||||
assertRows(execute("select format_time(col1, 'ms', 's') from %s where pk = 1"), row("1073741.83 s"));
|
||||
assertRows(execute("select format_time(col1, 'ms', 'm') from %s where pk = 1"), row("17895.7 m"));
|
||||
assertRows(execute("select format_time(col1, 'ms', 'h') from %s where pk = 1"), row("298.26 h"));
|
||||
assertRows(execute("select format_time(col1, 'ms', 'd') from %s where pk = 1"), row("12.43 d"));
|
||||
|
||||
assertRows(execute("select format_time(col1, 'd', 'd') from %s where pk = 2"), row("1 d"));
|
||||
assertRows(execute("select format_time(col1, 'd', 'h') from %s where pk = 2"), row("24 h"));
|
||||
assertRows(execute("select format_time(col1, 'd', 'm') from %s where pk = 2"), row("1440 m"));
|
||||
assertRows(execute("select format_time(col1, 'd', 's') from %s where pk = 2"), row("86400 s"));
|
||||
|
||||
assertRows(execute("select format_time(col1, 'd', 'd') from %s where pk = 3"), row("0 d"));
|
||||
assertRows(execute("select format_time(col1, 'd', 'h') from %s where pk = 3"), row("0 h"));
|
||||
assertRows(execute("select format_time(col1, 'd', 'm') from %s where pk = 3"), row("0 m"));
|
||||
assertRows(execute("select format_time(col1, 'd', 's') from %s where pk = 3"), row("0 s"));
|
||||
assertRows(execute("select format_time(col1, 'd', 'ms') from %s where pk = 3"), row("0 ms"));
|
||||
assertRows(execute("select format_time(col1, 'd', 'us') from %s where pk = 3"), row("0 us"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoOverflow()
|
||||
{
|
||||
createTable(of(BIGINT, INT, SMALLINT, TINYINT),
|
||||
new Object[][]{ { 1,
|
||||
Long.MAX_VALUE - 1,
|
||||
Integer.MAX_VALUE - 1,
|
||||
Short.MAX_VALUE - 1,
|
||||
Byte.MAX_VALUE - 1 },
|
||||
{ 2,
|
||||
Long.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
Short.MAX_VALUE,
|
||||
Byte.MAX_VALUE } });
|
||||
|
||||
// Won't overlfow because the value is one less than the Double.MAX_VALUE
|
||||
assertRows(execute("select format_time(col1, 'd', 'ns') from %s where pk = 1"), row("796899343984252600000000000000000 ns"));
|
||||
assertRows(execute("select format_time(col2, 'd', 'ns') from %s where pk = 1"), row("185542587014400000000000 ns"));
|
||||
assertRows(execute("select format_time(col3, 'd', 'ns') from %s where pk = 1"), row("2830982400000000000 ns"));
|
||||
assertRows(execute("select format_time(col4, 'd', 'ns') from %s where pk = 1"), row("10886400000000000 ns"));
|
||||
|
||||
assertRows(execute("select format_time(col1, 'd', 'ns') from %s where pk = 2"), row("796899343984252600000000000000000 ns"));
|
||||
assertRows(execute("select format_time(col2, 'd', 'ns') from %s where pk = 2"), row("185542587100800000000000 ns"));
|
||||
assertRows(execute("select format_time(col3, 'd', 'ns') from %s where pk = 2"), row("2831068800000000000 ns"));
|
||||
assertRows(execute("select format_time(col4, 'd', 'ns') from %s where pk = 2"), row("10972800000000000 ns"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllSupportedColumnTypes()
|
||||
{
|
||||
createTable(of(INT, TINYINT, SMALLINT, BIGINT, VARINT, ASCII, TEXT),
|
||||
new Object[][]{ { 1,
|
||||
Integer.MAX_VALUE,
|
||||
Byte.MAX_VALUE,
|
||||
Short.MAX_VALUE,
|
||||
Long.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
'\'' + Integer.valueOf(Integer.MAX_VALUE).toString() + '\'',
|
||||
'\'' + Integer.valueOf(Integer.MAX_VALUE).toString() + '\'',
|
||||
} });
|
||||
|
||||
assertRows(execute("select format_time(col1) from %s where pk = 1"), row("24.86 d"));
|
||||
assertRows(execute("select format_time(col2) from %s where pk = 1"), row("127 ms"));
|
||||
assertRows(execute("select format_time(col3) from %s where pk = 1"), row("32.77 s"));
|
||||
assertRows(execute("select format_time(col4) from %s where pk = 1"), row("106751991167.3 d"));
|
||||
assertRows(execute("select format_time(col5) from %s where pk = 1"), row("24.86 d"));
|
||||
assertRows(execute("select format_time(col6) from %s where pk = 1"), row("24.86 d"));
|
||||
assertRows(execute("select format_time(col7) from %s where pk = 1"), row("24.86 d"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeValueIsInvalid()
|
||||
{
|
||||
createDefaultTable(new Object[][]{ { "1", "-1", "-2" } });
|
||||
assertThatThrownBy(() -> execute("select format_time(col1) from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("value must be non-negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnparsableTextIsInvalid()
|
||||
{
|
||||
createTable(of(TEXT), new Object[][]{ { 1, "'abc'" }, { 2, "'-1'" } });
|
||||
|
||||
assertThatThrownBy(() -> execute("select format_time(col1) from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("unable to convert string 'abc' to a value of type long");
|
||||
|
||||
assertThatThrownBy(() -> execute("select format_time(col1) from %s where pk = 2"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("value must be non-negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidUnits()
|
||||
{
|
||||
createDefaultTable(new Object[][]{ { "1", "1", "2" } });
|
||||
for (String functionCall : new String[] {
|
||||
"format_time(col1, 'abc')",
|
||||
"format_time(col1, 'd', 'abc')",
|
||||
"format_time(col1, 'abc', 'd')",
|
||||
"format_time(col1, 'abc', 'abc')"
|
||||
})
|
||||
{
|
||||
assertThatThrownBy(() -> execute("select " + functionCall + " from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("Unsupported time unit: abc. Supported units are: ns, us, ms, s, m, h, d");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidArgumentsSize()
|
||||
{
|
||||
createDefaultTable(new Object[][]{ { "1", "1", "2" } });
|
||||
// test arguemnt size = 0
|
||||
assertThatThrownBy(() -> execute("select format_time() from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("Invalid number of arguments for function system.format_time([int|tinyint|smallint|bigint|varint|ascii|text], [ascii], [ascii])");
|
||||
|
||||
// Test argument size > 3
|
||||
assertThatThrownBy(() -> execute("select format_time(col1, 'ms', 's', 'h') from %s where pk = 1"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("Invalid number of arguments for function system.format_time([int|tinyint|smallint|bigint|varint|ascii|text], [ascii], [ascii])");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlingNullValues()
|
||||
{
|
||||
createTable(of(TEXT, ASCII, INT),
|
||||
new Object[][]{ { 1, null, null, null } });
|
||||
|
||||
assertRows(execute("select format_time(col1), format_time(col2), format_time(col3) from %s where pk = 1"),
|
||||
row(null, null, null));
|
||||
|
||||
assertRows(execute("select format_time(col1, 's') from %s where pk = 1"), row((Object) null));
|
||||
assertRows(execute("select format_time(col1, 's', 'd') from %s where pk = 1"), row((Object) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlingNullArguments()
|
||||
{
|
||||
createTable(of(TEXT, ASCII, INT),
|
||||
new Object[][]{ { 1, null, null, null },
|
||||
{ 2, "'1'", "'2'", 3 } });
|
||||
|
||||
assertRows(execute("select format_time(col1, null) from %s where pk = 1"), row((Object) null));
|
||||
|
||||
for (String functionCall : new String[] {
|
||||
"format_time(col3, null)",
|
||||
"format_time(col3, null, null)",
|
||||
"format_time(col3, null, 'd')",
|
||||
"format_time(col3, 'd', null)"
|
||||
})
|
||||
{
|
||||
assertThatThrownBy(() -> execute("select " + functionCall + " from %s where pk = 2"))
|
||||
.isInstanceOf(InvalidRequestException.class)
|
||||
.hasMessageContaining("none of the arguments may be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFuzzRandomGenerators()
|
||||
{
|
||||
createTable("CREATE TABLE %s (pk int primary key, col1 int)");
|
||||
qt().withExamples(1024).forAll(integers().allPositive()).checkAssert(
|
||||
(randInt) -> {
|
||||
execute("INSERT INTO %s (pk, col1) VALUES (?, ?)", 1, randInt);
|
||||
assertRows(execute("select format_time(col1, 's', 'm') from %s where pk = 1"), row(format((double) randInt * (1 / 60.0)) + " m"));
|
||||
assertRows(execute("select format_time(col1, 's', 'h') from %s where pk = 1"), row(format((double) randInt * (1 / 3600.0)) + " h"));
|
||||
assertRows(execute("select format_time(col1, 's', 'd') from %s where pk = 1"), row(format((double) randInt * (1 / 86400.0)) + " d"));
|
||||
assertRows(execute("select format_time(col1, 'ms', 'm') from %s where pk = 1"), row(format((double) randInt * (1 / (60 * 1000.0))) + " m"));
|
||||
assertRows(execute("select format_time(col1, 'ms', 'h') from %s where pk = 1"), row(format((double) randInt * (1 / (3600 * 1000.0))) + " h"));
|
||||
assertRows(execute("select format_time(col1, 'ms', 'd') from %s where pk = 1"), row(format((double) randInt * (1 / (86400 * 1000.0))) + " d"));
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue