mirror of
https://github.com/taosdata/TDengine
synced 2026-05-24 10:09:01 +00:00
1495 lines
57 KiB
Python
1495 lines
57 KiB
Python
import math
|
|
from random import randrange
|
|
import random
|
|
import time
|
|
import threading
|
|
import secrets
|
|
import numpy
|
|
from pandas.compat import is_platform_arm
|
|
|
|
from stable import query_after_reset
|
|
from util.log import *
|
|
from util.sql import *
|
|
from util.cases import *
|
|
from util.dnodes import *
|
|
from util.common import *
|
|
from decimal import *
|
|
from multiprocessing import Value, Lock
|
|
from functools import cmp_to_key
|
|
|
|
class AtomicCounter:
|
|
def __init__(self, initial_value=0):
|
|
self._value = Value('i', initial_value)
|
|
self._lock = Lock()
|
|
|
|
def fetch_add(self, delta = 1):
|
|
with self._lock:
|
|
old_value = self._value.value
|
|
self._value.value += delta
|
|
return old_value
|
|
|
|
getcontext().prec = 40
|
|
|
|
def get_decimal(val, scale: int):
|
|
if val == 'NULL':
|
|
return None
|
|
getcontext().prec = 100
|
|
try:
|
|
return Decimal(val).quantize(Decimal("1." + "0" * scale), ROUND_HALF_UP)
|
|
except:
|
|
tdLog.exit(f"faield to convert to decimal for v: {val} scale: {scale}")
|
|
|
|
syntax_error = -2147473920
|
|
invalid_column = -2147473918
|
|
invalid_compress_level = -2147483084
|
|
invalid_encode_param = -2147483087
|
|
invalid_operation = -2147483136
|
|
scalar_convert_err = -2147470768
|
|
|
|
|
|
decimal_test_query = True
|
|
decimal_insert_validator_test = True
|
|
operator_test_round = 1
|
|
tb_insert_rows = 1000
|
|
binary_op_with_const_test = True
|
|
binary_op_with_col_test = True
|
|
unary_op_test = True
|
|
binary_op_in_where_test = True
|
|
test_decimal_funcs = False
|
|
cast_func_test_round = 10
|
|
in_op_test_round = 10
|
|
|
|
null_ratio = 0.8
|
|
test_round = 100
|
|
|
|
class DecimalTypeGeneratorConfig:
|
|
def __init__(self):
|
|
self.enable_weight_overflow: bool = False
|
|
self.weightOverflowRatio: float = 0.001
|
|
self.enable_scale_overflow: bool = True
|
|
self.scale_overflow_ratio = 0.1
|
|
self.enable_positive_sign = False
|
|
self.with_corner_case = True
|
|
self.corner_case_ratio = null_ratio
|
|
self.positive_ratio = 0.7
|
|
self.prec = 38
|
|
self.scale = 10
|
|
|
|
|
|
class DecimalStringRandomGenerator:
|
|
def __init__(self):
|
|
self.corner_cases = ["NULL", "0"]
|
|
self.ratio_base: int = 1000000
|
|
|
|
def possible(self, possibility: float) -> bool:
|
|
return random.randint(0, self.ratio_base) < possibility * self.ratio_base
|
|
|
|
def generate_sign(self, positive_ratio: float) -> str:
|
|
if self.possible(positive_ratio):
|
|
return "+"
|
|
return "-"
|
|
|
|
def generate_digit(self) -> str:
|
|
return str(random.randint(0, 9))
|
|
|
|
def current_should_generate_corner_case(self, corner_case_ratio: float) -> bool:
|
|
return self.possible(corner_case_ratio)
|
|
|
|
def generate_corner_case(self, config: DecimalTypeGeneratorConfig) -> str:
|
|
if self.possible(0.8):
|
|
return random.choice(self.corner_cases)
|
|
else:
|
|
res = self.generate_digit() * (config.prec - config.scale)
|
|
if self.possible(0.8):
|
|
res += "."
|
|
if self.possible(0.8):
|
|
res += self.generate_digit() * config.scale
|
|
return res
|
|
|
|
## 写入大整数的例子, 如10000000000, scale解析时可能为负数
|
|
## Generate decimal with E/e
|
|
def generate(self, config: DecimalTypeGeneratorConfig) -> str:
|
|
ret: str = ""
|
|
sign = self.generate_sign(config.positive_ratio)
|
|
if config.with_corner_case and self.current_should_generate_corner_case(
|
|
config.corner_case_ratio
|
|
):
|
|
ret += self.generate_corner_case(config)
|
|
else:
|
|
if config.enable_positive_sign or sign != "+":
|
|
ret += sign
|
|
weight = random.randint(1, config.prec - config.scale)
|
|
scale = random.randint(1, config.scale)
|
|
for i in range(weight):
|
|
ret += self.generate_digit()
|
|
|
|
if config.enable_weight_overflow and self.possible(
|
|
config.weightOverflowRatio
|
|
):
|
|
extra_weight = (
|
|
config.prec
|
|
- weight
|
|
+ 1
|
|
+ random.randint(1, self.get_max_prec(config.prec))
|
|
)
|
|
while extra_weight > 0:
|
|
ret += self.generate_digit()
|
|
extra_weight -= 1
|
|
ret += "."
|
|
for i in range(scale):
|
|
ret += self.generate_digit()
|
|
if config.enable_scale_overflow and self.possible(
|
|
config.scale_overflow_ratio
|
|
):
|
|
extra_scale = (
|
|
config.scale
|
|
- scale
|
|
+ 1
|
|
+ random.randint(1, self.get_max_prec(config.prec))
|
|
)
|
|
while extra_scale > 0:
|
|
ret += self.generate_digit()
|
|
extra_scale -= 1
|
|
return ret
|
|
|
|
def get_max_prec(self, prec):
|
|
if prec <= 18:
|
|
return 18
|
|
else:
|
|
return 38
|
|
|
|
|
|
class DecimalColumnAggregator:
|
|
def __init__(self):
|
|
self.max: Decimal = Decimal("0")
|
|
self.min: Decimal = Decimal("0")
|
|
self.count: int = 0
|
|
self.sum: Decimal = Decimal("0")
|
|
self.null_num: int = 0
|
|
self.none_num: int = 0
|
|
self.first = None
|
|
self.last = None
|
|
|
|
def add_value(self, value: str, scale: int):
|
|
self.count += 1
|
|
if value == "NULL":
|
|
self.null_num += 1
|
|
elif value == "None":
|
|
self.none_num += 1
|
|
else:
|
|
v: Decimal = get_decimal(value, scale)
|
|
if self.first is None:
|
|
self.first = v
|
|
self.last = v
|
|
self.sum += v
|
|
if v > self.max:
|
|
self.max = v
|
|
if v < self.min:
|
|
self.min = v
|
|
|
|
atomic_counter = AtomicCounter(0)
|
|
|
|
class TypeEnum:
|
|
BOOL = 1
|
|
TINYINT = 2
|
|
SMALLINT = 3
|
|
INT = 4
|
|
BIGINT = 5
|
|
FLOAT = 6
|
|
DOUBLE = 7
|
|
VARCHAR = 8
|
|
TIMESTAMP = 9
|
|
NCHAR = 10
|
|
UTINYINT = 11
|
|
USMALLINT = 12
|
|
UINT = 13
|
|
UBIGINT = 14
|
|
JSON = 15
|
|
VARBINARY = 16
|
|
DECIMAL = 17
|
|
BINARY = 8
|
|
GEOMETRY = 20
|
|
DECIMAL64 = 21
|
|
|
|
@staticmethod
|
|
def get_type_prec(type: int):
|
|
type_prec = [0, 1, 3, 5, 10, 19, 38, 38, 0, 19, 10, 3, 5, 10, 20, 0, 0, 0, 0, 0, 0, 0]
|
|
return type_prec[type]
|
|
|
|
@staticmethod
|
|
def get_type_str(type: int):
|
|
type_str = [
|
|
"",
|
|
"BOOL",
|
|
"TINYINT",
|
|
"SMALLINT",
|
|
"INT",
|
|
"BIGINT",
|
|
"FLOAT",
|
|
"DOUBLE",
|
|
"VARCHAR",
|
|
"TIMESTAMP",
|
|
"NCHAR",
|
|
"TINYINT UNSIGNED",
|
|
"SMALLINT UNSIGNED",
|
|
"INT UNSIGNED",
|
|
"BIGINT UNSIGNED",
|
|
"JSON",
|
|
"VARBINARY",
|
|
"DECIMAL",
|
|
"",
|
|
"",
|
|
"GEOMETRY",
|
|
"DECIMAL",
|
|
]
|
|
return type_str[type]
|
|
|
|
|
|
class DataType:
|
|
def __init__(self, type: int, length: int = 0, type_mod: int = 0):
|
|
self.type: int = type
|
|
self.length = length
|
|
self.type_mod = type_mod
|
|
|
|
def __str__(self):
|
|
if self.type_mod != 0:
|
|
return f"{TypeEnum.get_type_str(self.type)}({self.prec()}, {self.scale()})"
|
|
if self.length:
|
|
return f"{TypeEnum.get_type_str(self.type)}({self.length})"
|
|
return TypeEnum.get_type_str(self.type)
|
|
|
|
def __eq__(self, other):
|
|
return self.type == other.type and self.length == other.length and self.type_mod == other.type_mod
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __hash__(self):
|
|
return hash((self.type, self.length))
|
|
|
|
def __repr__(self):
|
|
return f"DataType({self.type}, {self.length}, {self.type_mod})"
|
|
|
|
def is_decimal_type(self):
|
|
return self.type == TypeEnum.DECIMAL or self.type == TypeEnum.DECIMAL64
|
|
|
|
def is_varchar_type(self):
|
|
return self.type == TypeEnum.VARCHAR or self.type == TypeEnum.NCHAR or self.type == TypeEnum.VARBINARY or self.type == TypeEnum.JSON or self.type == TypeEnum.BINARY
|
|
|
|
def is_real_type(self):
|
|
return self.type == TypeEnum.FLOAT or self.type == TypeEnum.DOUBLE
|
|
|
|
def prec(self):
|
|
return 0
|
|
|
|
def scale(self):
|
|
return 0
|
|
|
|
## TODO generate NULL, None
|
|
def generate_value(self) -> str:
|
|
if random.randint(0, 100000) < null_ratio * 100000:
|
|
return 'NULL'
|
|
if self.type == TypeEnum.BOOL:
|
|
return ['true', 'false'][secrets.randbelow(2)]
|
|
if self.type == TypeEnum.TINYINT:
|
|
return str(secrets.randbelow(256) - 128)
|
|
if self.type == TypeEnum.SMALLINT:
|
|
return str(secrets.randbelow(65536) - 32768)
|
|
if self.type == TypeEnum.INT:
|
|
return str(secrets.randbelow(4294967296) - 2147483648)
|
|
if self.type == TypeEnum.BIGINT:
|
|
return str(secrets.randbelow(9223372036854775808) - 4611686018427387904)
|
|
if self.type == TypeEnum.FLOAT or self.type == TypeEnum.DOUBLE:
|
|
return str(random.uniform(-1e10, 1e10))
|
|
if (
|
|
self.type == TypeEnum.VARCHAR
|
|
or self.type == TypeEnum.NCHAR
|
|
or self.type == TypeEnum.VARBINARY
|
|
):
|
|
return f"'{str(random.uniform(-1e20, 1e20))[0:self.length]}'"
|
|
if self.type == TypeEnum.TIMESTAMP:
|
|
return str(secrets.randbelow(9223372036854775808))
|
|
if self.type == TypeEnum.UTINYINT:
|
|
return str(secrets.randbelow(256))
|
|
if self.type == TypeEnum.USMALLINT:
|
|
return str(secrets.randbelow(65536))
|
|
if self.type == TypeEnum.UINT:
|
|
return str(secrets.randbelow(4294967296))
|
|
if self.type == TypeEnum.UBIGINT:
|
|
return str(secrets.randbelow(9223372036854775808))
|
|
if self.type == TypeEnum.JSON:
|
|
return f'{{"key": "{secrets.token_urlsafe(10)}"}}'
|
|
if self.type == TypeEnum.GEOMETRY:
|
|
return "'POINT(1.0 1.0)'"
|
|
raise Exception(f"unsupport type {self.type}")
|
|
|
|
def generate_sized_val(self, prec: int, scale: int) -> str:
|
|
weight = prec - scale
|
|
if self.type == TypeEnum.BOOL:
|
|
return ['true', 'false'][secrets.randbelow(2)]
|
|
if self.type == TypeEnum.TINYINT or self.type == TypeEnum.SMALLINT or self.type == TypeEnum.INT or self.type == TypeEnum.BIGINT or self.type == TypeEnum.TIMESTAMP:
|
|
return str(secrets.randbelow(10 * weight * 2) - 10 * weight)
|
|
if self.type == TypeEnum.FLOAT or self.type == TypeEnum.DOUBLE:
|
|
return str(random.uniform(-10 * weight, 10 * weight))
|
|
if (
|
|
self.type == TypeEnum.VARCHAR
|
|
or self.type == TypeEnum.NCHAR
|
|
or self.type == TypeEnum.VARBINARY
|
|
):
|
|
return f"'{str(random.uniform(-(10 * weight + 1), 10 * weight - 1))}'"
|
|
if self.type == TypeEnum.UTINYINT or self.type == TypeEnum.USMALLINT or self.type == TypeEnum.UINT or self.type == TypeEnum.UBIGINT:
|
|
return str(secrets.randbelow(10 * weight * 2))
|
|
if self.type == TypeEnum.JSON:
|
|
return f'{{"key": "{secrets.token_urlsafe(10)}"}}'
|
|
if self.type == TypeEnum.GEOMETRY:
|
|
return "'POINT(1.0 1.0)'"
|
|
raise Exception(f"unsupport type {self.type}")
|
|
|
|
|
|
def check(self, values, offset: int):
|
|
return True
|
|
|
|
def get_typed_val_for_execute(self, val, const_col = False):
|
|
if self.type == TypeEnum.DOUBLE:
|
|
return float(val)
|
|
elif self.type == TypeEnum.BOOL:
|
|
if val == "true":
|
|
return 1
|
|
else:
|
|
return 0
|
|
elif self.type == TypeEnum.FLOAT:
|
|
if const_col:
|
|
val = float(str(numpy.float32(val)))
|
|
else:
|
|
val = float(numpy.float32(val))
|
|
elif self.type == TypeEnum.DECIMAL or self.type == TypeEnum.DECIMAL64:
|
|
return get_decimal(val, self.scale())
|
|
elif isinstance(val, str):
|
|
val = val.strip("'")
|
|
if len(val) == 0:
|
|
return 0
|
|
return val
|
|
|
|
def get_typed_val(self, val):
|
|
if self.type == TypeEnum.FLOAT:
|
|
return float(str(numpy.float32(val)))
|
|
elif self.type == TypeEnum.DOUBLE:
|
|
return float(val)
|
|
return val
|
|
|
|
@staticmethod
|
|
def get_decimal_types() -> list:
|
|
return [TypeEnum.DECIMAL64, TypeEnum.DECIMAL]
|
|
|
|
@staticmethod
|
|
def get_decimal_op_types()-> list:
|
|
return [
|
|
TypeEnum.BOOL,
|
|
TypeEnum.TINYINT,
|
|
TypeEnum.SMALLINT,
|
|
TypeEnum.INT,
|
|
TypeEnum.BIGINT,
|
|
TypeEnum.FLOAT,
|
|
TypeEnum.DOUBLE,
|
|
TypeEnum.VARCHAR,
|
|
TypeEnum.NCHAR,
|
|
TypeEnum.UTINYINT,
|
|
TypeEnum.USMALLINT,
|
|
TypeEnum.UINT,
|
|
TypeEnum.UBIGINT,
|
|
TypeEnum.DECIMAL,
|
|
TypeEnum.DECIMAL64,
|
|
]
|
|
|
|
@staticmethod
|
|
def generate_random_type_for(dt: int):
|
|
if dt == TypeEnum.DECIMAL:
|
|
prec = random.randint(1, DecimalType.DECIMAL_MAX_PRECISION)
|
|
return DecimalType(dt, prec, random.randint(0, prec))
|
|
elif dt == TypeEnum.DECIMAL64:
|
|
prec = random.randint(1, DecimalType.DECIMAL64_MAX_PRECISION)
|
|
return DecimalType(dt, prec, random.randint(0, prec))
|
|
elif dt == TypeEnum.BINARY or dt == TypeEnum.VARCHAR:
|
|
return DataType(dt, random.randint(16, 255), 0)
|
|
else:
|
|
return DataType(dt, 0, 0)
|
|
|
|
class DecimalType(DataType):
|
|
DECIMAL_MAX_PRECISION = 38
|
|
DECIMAL64_MAX_PRECISION = 18
|
|
def __init__(self, type, precision: int, scale: int):
|
|
self.precision_ = precision
|
|
self.scale_ = scale
|
|
if type == TypeEnum.DECIMAL64:
|
|
bytes = 8
|
|
else:
|
|
bytes = 16
|
|
super().__init__(type, bytes, self.get_decimal_type_mod())
|
|
self.decimal_generator: DecimalStringRandomGenerator = DecimalStringRandomGenerator()
|
|
self.generator_config: DecimalTypeGeneratorConfig = DecimalTypeGeneratorConfig()
|
|
#self.generator_config.with_corner_case = False
|
|
self.generator_config.prec = precision
|
|
self.generator_config.scale = scale
|
|
self.aggregator: DecimalColumnAggregator = DecimalColumnAggregator()
|
|
self.values: List[str] = []
|
|
|
|
def get_decimal_type_mod(self) -> int:
|
|
return self.precision_ * 100 + self.scale()
|
|
|
|
def set_prec(self, prec: int):
|
|
self.precision_ = prec
|
|
self.type_mod = self.get_decimal_type_mod()
|
|
|
|
def set_scale(self, scale: int):
|
|
self.scale_ = scale
|
|
self.type_mod = self.get_decimal_type_mod()
|
|
|
|
def prec(self):
|
|
return self.precision_
|
|
|
|
def scale(self):
|
|
return self.scale_
|
|
|
|
def __str__(self):
|
|
return f"DECIMAL({self.precision_}, {self.scale()})"
|
|
|
|
def __eq__(self, other: DataType):
|
|
return self.precision_ == other.prec() and self.scale() == other.scale()
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __hash__(self):
|
|
return hash((self.precision_, self.scale()))
|
|
|
|
def __repr__(self):
|
|
return f"DecimalType({self.precision_}, {self.scale()})"
|
|
|
|
def generate_value(self) -> str:
|
|
val = self.decimal_generator.generate(self.generator_config)
|
|
self.aggregator.add_value(val, self.scale()) ## convert to Decimal first
|
|
# self.values.append(val) ## save it into files maybe
|
|
return val
|
|
|
|
def get_typed_val(self, val):
|
|
if val == "NULL":
|
|
return None
|
|
return get_decimal(val, self.scale())
|
|
|
|
def get_typed_val_for_execute(self, val, const_col = False):
|
|
return self.get_typed_val(val)
|
|
|
|
@staticmethod
|
|
def default_compression() -> str:
|
|
return "zstd"
|
|
|
|
@staticmethod
|
|
def default_encode() -> str:
|
|
return "disabled"
|
|
|
|
def check(self, values, offset: int):
|
|
val_from_query = values
|
|
val_insert = self.values[offset:]
|
|
for v_from_query, v_from_insert in zip(val_from_query, val_insert):
|
|
if v_from_insert == "NULL":
|
|
if v_from_query.strip() != "NULL":
|
|
tdLog.debug(
|
|
f"val_insert: {val_insert} val_from_query: {val_from_query}"
|
|
)
|
|
tdLog.exit(f"insert NULL, query not NULL: {v_from_query}")
|
|
else:
|
|
continue
|
|
try:
|
|
dec_query: Decimal = Decimal(v_from_query)
|
|
dec_insert: Decimal = Decimal(v_from_insert)
|
|
dec_insert = get_decimal(dec_insert, self.scale())
|
|
except Exception as e:
|
|
tdLog.exit(f"failed to convert {v_from_query} or {v_from_insert} to decimal, {e}")
|
|
return False
|
|
if dec_query != dec_insert:
|
|
tdLog.exit(
|
|
f"check decimal column failed for insert: {v_from_insert}, query: {v_from_query}, expect {dec_insert}, but get {dec_query}"
|
|
)
|
|
return False
|
|
else:
|
|
tdLog.debug(
|
|
f"check decimal succ, insert:{v_from_insert} query:{v_from_query}, py dec: {dec_insert}"
|
|
)
|
|
|
|
|
|
@staticmethod
|
|
def decimal_type_from_other_type(other: DataType):
|
|
prec = 0
|
|
return DecimalType(other.type, other.length, other.type_mod)
|
|
|
|
class Column:
|
|
def __init__(self, type: DataType):
|
|
self.type_: DataType = type
|
|
self.name_: str = ""
|
|
self.saved_vals:dict[str:[]] = {}
|
|
|
|
def is_constant_col(self):
|
|
return '' in self.saved_vals.keys()
|
|
|
|
def get_typed_val(self, val):
|
|
return self.type_.get_typed_val(val)
|
|
|
|
def get_typed_val_for_execute(self, val, const_col = False):
|
|
return self.type_.get_typed_val_for_execute(val, const_col)
|
|
|
|
def get_constant_val(self):
|
|
return self.get_typed_val(self.saved_vals[''][0])
|
|
|
|
def get_constant_val_for_execute(self):
|
|
return self.get_typed_val_for_execute(self.saved_vals[''][0], const_col=True)
|
|
|
|
def __str__(self):
|
|
if self.is_constant_col():
|
|
return str(self.get_constant_val())
|
|
return self.name_
|
|
|
|
def get_val_for_execute(self, tbname: str, idx: int):
|
|
if self.is_constant_col():
|
|
return self.get_constant_val_for_execute()
|
|
if len(self.saved_vals) > 1:
|
|
for key in self.saved_vals.keys():
|
|
l = len(self.saved_vals[key])
|
|
if idx < l:
|
|
return self.get_typed_val_for_execute(self.saved_vals[key][idx])
|
|
else:
|
|
idx -= l
|
|
return self.get_typed_val_for_execute(self.saved_vals[tbname][idx])
|
|
|
|
def get_cardinality(self, tbname):
|
|
if self.is_constant_col():
|
|
return 1
|
|
elif len(self.saved_vals) > 1:
|
|
return len(self.saved_vals['t0'])
|
|
else:
|
|
return len(self.saved_vals[tbname])
|
|
|
|
def seq_scan_col(self, tbname: str, idx: int):
|
|
if self.is_constant_col():
|
|
return self.get_constant_val_for_execute(), False
|
|
elif len(self.saved_vals) > 1:
|
|
keys = list(self.saved_vals.keys())
|
|
for i, key in enumerate(keys):
|
|
l = len(self.saved_vals[key])
|
|
if idx < l:
|
|
return self.get_typed_val_for_execute(self.saved_vals[key][idx]), True
|
|
else:
|
|
idx -= l
|
|
|
|
return 1, False
|
|
else:
|
|
if idx > len(self.saved_vals[tbname]) - 1:
|
|
return 1, False
|
|
v = self.get_typed_val_for_execute(self.saved_vals[tbname][idx])
|
|
return v, True
|
|
|
|
@staticmethod
|
|
def comp_key(key1, key2):
|
|
if key1 is None:
|
|
return -1
|
|
if key2 is None:
|
|
return 1
|
|
return key1 - key2
|
|
|
|
def get_ordered_result(self, tbname: str, asc: bool) -> list:
|
|
if tbname in self.saved_vals:
|
|
return sorted(
|
|
[
|
|
get_decimal(val, self.type_.scale())
|
|
for val in self.saved_vals[tbname]
|
|
],
|
|
reverse=not asc,
|
|
key=cmp_to_key(Column.comp_key)
|
|
)
|
|
else:
|
|
res = []
|
|
for val in self.saved_vals.values():
|
|
res.extend(val)
|
|
return sorted(
|
|
[get_decimal(val, self.type_.scale()) for val in res], reverse=not asc,
|
|
key=cmp_to_key(Column.comp_key)
|
|
)
|
|
|
|
def get_group_num(self, tbname, ignore_null=False) -> int:
|
|
if tbname in self.saved_vals:
|
|
s = set(get_decimal(val, self.type_.scale()) for val in self.saved_vals[tbname])
|
|
if ignore_null:
|
|
s.remove(None)
|
|
return len(s)
|
|
else:
|
|
res = set()
|
|
for vals in self.saved_vals.values():
|
|
for v in vals:
|
|
res.add(get_decimal(v, self.type_.scale()))
|
|
if ignore_null:
|
|
res.remove(None)
|
|
return len(res)
|
|
|
|
## tbName: for normal table, pass the tbname, for child table, pass the child table name
|
|
def generate_value(self, tbName: str = '', save: bool = True):
|
|
val = self.type_.generate_value()
|
|
if save:
|
|
if tbName not in self.saved_vals:
|
|
self.saved_vals[tbName] = []
|
|
## for constant columns, always replace the last val
|
|
if self.is_constant_col():
|
|
self.saved_vals[tbName] = [val]
|
|
else:
|
|
self.saved_vals[tbName].append(val)
|
|
return val
|
|
|
|
def get_type_str(self) -> str:
|
|
return str(self.type_)
|
|
|
|
def set_name(self, name: str):
|
|
self.name_ = name
|
|
|
|
def check(self, values, offset: int):
|
|
return self.type_.check(values, offset)
|
|
|
|
def construct_type_value(self, val: str):
|
|
if (
|
|
self.type_.type == TypeEnum.BINARY
|
|
or self.type_.type == TypeEnum.VARCHAR
|
|
or self.type_.type == TypeEnum.NCHAR
|
|
or self.type_.type == TypeEnum.VARBINARY
|
|
or self.type_.type == TypeEnum.JSON
|
|
):
|
|
return f"'{val}'"
|
|
else:
|
|
return val
|
|
|
|
@staticmethod
|
|
def get_decimal_unsupported_types() -> list:
|
|
return [
|
|
TypeEnum.JSON,
|
|
TypeEnum.GEOMETRY,
|
|
TypeEnum.VARBINARY,
|
|
]
|
|
|
|
@staticmethod
|
|
def get_decimal_oper_const_cols() -> list:
|
|
types_unable_to_be_const = [
|
|
TypeEnum.TINYINT,
|
|
TypeEnum.SMALLINT,
|
|
TypeEnum.INT,
|
|
TypeEnum.UINT,
|
|
TypeEnum.USMALLINT,
|
|
TypeEnum.UTINYINT,
|
|
TypeEnum.UBIGINT,
|
|
]
|
|
return Column.get_all_type_columns(
|
|
Column.get_decimal_unsupported_types()
|
|
+ Column.get_decimal_types()
|
|
+ types_unable_to_be_const
|
|
)
|
|
|
|
@staticmethod
|
|
def get_decimal_types() -> List:
|
|
return [TypeEnum.DECIMAL, TypeEnum.DECIMAL64]
|
|
|
|
@staticmethod
|
|
def get_all_type_columns(types_to_exclude: List[TypeEnum] = []) -> List:
|
|
all_types = [
|
|
Column(DataType(TypeEnum.BOOL)),
|
|
Column(DataType(TypeEnum.TINYINT)),
|
|
Column(DataType(TypeEnum.SMALLINT)),
|
|
Column(DataType(TypeEnum.INT)),
|
|
Column(DataType(TypeEnum.BIGINT)),
|
|
Column(DataType(TypeEnum.FLOAT)),
|
|
Column(DataType(TypeEnum.DOUBLE)),
|
|
Column(DataType(TypeEnum.VARCHAR, 255)),
|
|
Column(DataType(TypeEnum.TIMESTAMP)),
|
|
Column(DataType(TypeEnum.NCHAR, 255)),
|
|
Column(DataType(TypeEnum.UTINYINT)),
|
|
Column(DataType(TypeEnum.USMALLINT)),
|
|
Column(DataType(TypeEnum.UINT)),
|
|
Column(DataType(TypeEnum.UBIGINT)),
|
|
Column(DataType(TypeEnum.JSON)),
|
|
Column(DataType(TypeEnum.VARBINARY, 255)),
|
|
Column(DecimalType(TypeEnum.DECIMAL, 38, 10)),
|
|
Column(DataType(TypeEnum.BINARY, 255)),
|
|
Column(DataType(TypeEnum.GEOMETRY, 10240)),
|
|
Column(DecimalType(TypeEnum.DECIMAL64, 18, 4)),
|
|
]
|
|
ret = []
|
|
for c in all_types:
|
|
found = False
|
|
for type in types_to_exclude:
|
|
if c.type_.type == type:
|
|
found = True
|
|
break
|
|
if not found:
|
|
ret.append(c)
|
|
return ret
|
|
|
|
|
|
class DecimalColumnTableCreater:
|
|
def __init__(
|
|
self,
|
|
conn,
|
|
dbName: str,
|
|
tbName: str,
|
|
columns: List[Column],
|
|
tags_cols: List[Column] = [],
|
|
col_prefix: str = "c",
|
|
tag_prefix: str = "t",
|
|
):
|
|
self.conn = conn
|
|
self.dbName = dbName
|
|
self.tbName = tbName
|
|
self.tags_cols = tags_cols
|
|
self.columns: List[Column] = columns
|
|
self.col_prefix = col_prefix
|
|
self.tag_prefix = tag_prefix
|
|
|
|
def create(self):
|
|
if len(self.tags_cols) > 0:
|
|
table = "stable"
|
|
else:
|
|
table = "table"
|
|
sql = f"create {table} {self.dbName}.{self.tbName} (ts timestamp"
|
|
for i, column in enumerate(self.columns):
|
|
tbname = f"{self.col_prefix}{i+1}"
|
|
sql += f", {tbname} {column.get_type_str()}"
|
|
column.set_name(tbname)
|
|
if self.tags_cols:
|
|
sql += ") tags("
|
|
for i, tag in enumerate(self.tags_cols):
|
|
tagname = f"{self.tag_prefix}{i+1}"
|
|
sql += f"{tagname} {tag.get_type_str()}"
|
|
tag.set_name(tagname)
|
|
if i != len(self.tags_cols) - 1:
|
|
sql += ", "
|
|
sql += ")"
|
|
self.conn.execute(sql, queryTimes=1)
|
|
|
|
def create_child_table(
|
|
self,
|
|
ctbPrefix: str,
|
|
ctbNum: int,
|
|
tag_cols: List[Column],
|
|
tag_values: List[str],
|
|
):
|
|
for i in range(ctbNum):
|
|
tbname = f"{ctbPrefix}{i}"
|
|
sql = f"create table {self.dbName}.{tbname} using {self.dbName}.{self.tbName} tags("
|
|
for j, tag in enumerate(tag_cols):
|
|
sql += f"{tag.construct_type_value(tag_values[j])}"
|
|
if j != len(tag_cols) - 1:
|
|
sql += ", "
|
|
sql += ")"
|
|
self.conn.execute(sql, queryTimes=1)
|
|
|
|
|
|
class TableInserter:
|
|
def __init__(
|
|
self,
|
|
conn,
|
|
dbName: str,
|
|
tbName: str,
|
|
columns: List[Column],
|
|
tags_cols: List[Column] = [],
|
|
):
|
|
self.conn: TDSql = conn
|
|
self.dbName = dbName
|
|
self.tbName = tbName
|
|
self.tag_cols = tags_cols
|
|
self.columns = columns
|
|
|
|
def insert(self, rows: int, start_ts: int, step: int, flush_database: bool = False):
|
|
step = random.randint(step / 2, step * 1.5)
|
|
pre_insert = f"insert into {self.dbName}.{self.tbName} values"
|
|
sql = pre_insert
|
|
for i in range(rows):
|
|
sql += f"({start_ts + i * step}"
|
|
for column in self.columns:
|
|
sql += f", {column.generate_value(self.tbName)}"
|
|
sql += ")"
|
|
if i != rows - 1:
|
|
sql += ", "
|
|
local_flush_database = i % 5000 == 0
|
|
if len(sql) > 1000:
|
|
# tdLog.debug(f"insert into with sql{sql}")
|
|
if flush_database and local_flush_database:
|
|
self.conn.execute(f"flush database {self.dbName}", queryTimes=1)
|
|
self.conn.execute(sql, queryTimes=1)
|
|
sql = pre_insert
|
|
if len(sql) > len(pre_insert):
|
|
# tdLog.debug(f"insert into with sql{sql}")
|
|
if flush_database:
|
|
self.conn.execute(f"flush database {self.dbName}", queryTimes=1)
|
|
self.conn.execute(sql, queryTimes=1)
|
|
|
|
class FillQueryGenerator:
|
|
def __init__(self, dbname: str, tbname:str, columns: list, ts_start, ts_end):
|
|
self.dbname = dbname
|
|
self.tbname = tbname
|
|
self.columns: list[Column] = columns
|
|
self.ts_start: int = ts_start
|
|
self.ts_end: int = ts_end
|
|
self.interval = None
|
|
self.select = None
|
|
self.partition = None
|
|
self.fill = None
|
|
self.where = None
|
|
self.where_start = None
|
|
self.where_end = None
|
|
self.window_count = None
|
|
|
|
def is_partition(self) -> bool:
|
|
return self.partition != ''
|
|
|
|
def get_agg_funcs(self) ->list:
|
|
res: list[str] = []
|
|
funcs = ['avg', 'min', 'max', 'sum', 'count']
|
|
decimal_funcs = ['min', 'max', 'count']
|
|
for col in self.columns:
|
|
if not col.type_.is_varchar_type():
|
|
if col.type_.is_decimal_type():
|
|
#res.append(random.choice(decimal_funcs) + f'({col.name_})')
|
|
pass
|
|
else:
|
|
res.append(random.choice(funcs) + f'({col.name_})')
|
|
return res
|
|
|
|
def generate_select_list(self):
|
|
self.generate_partition_by()
|
|
if self.select is None:
|
|
select = '_wstart, '
|
|
funcs = self.get_agg_funcs()
|
|
if self.is_partition():
|
|
funcs.append('tbname')
|
|
select += ','.join(funcs)
|
|
self.select = select
|
|
return self.select
|
|
|
|
def generate_partition_by(self):
|
|
if self.partition is None:
|
|
partition_by_tbname = random.randint(0, 10)
|
|
if partition_by_tbname < 5 and self.generate_fill() != 'NULL_F':
|
|
self.partition = 'partition by tbname'
|
|
else:
|
|
self.partition = ''
|
|
return self.partition
|
|
|
|
def generate_interval(self):
|
|
if self.interval is None:
|
|
self.interval = random.randint(1, 40000)
|
|
return self.interval
|
|
|
|
def generate_fill(self):
|
|
fill = ['PREV', 'NEXT', 'NULL', 'LINEAR', 'NULL_F']
|
|
if self.fill is None:
|
|
self.fill = random.choice(fill)
|
|
return self.fill
|
|
|
|
def fix_where_out_of_interval_range(self):
|
|
return
|
|
if self.where_end < self.ts_start or self.where_start > self.ts_end:
|
|
self.where = None
|
|
self.generate_where()
|
|
|
|
def generate_where(self):
|
|
if self.where is None:
|
|
where = 'ts '
|
|
if self.tbname == 'meters':
|
|
window_count = random.randint(1, 10000)
|
|
else:
|
|
window_count = random.randint(1, 50000)
|
|
ts_start = self.ts_start - window_count * self.generate_interval() * 1.2
|
|
ts_end = self.ts_end + window_count * self.generate_interval() * 1.2
|
|
ts_start = random.randint(int(ts_start), int(ts_end))
|
|
ts_end = ts_start + window_count * self.generate_interval()
|
|
self.where_start = ts_start
|
|
self.where_end = ts_end
|
|
self.where = f'ts >= {ts_start} and ts < {ts_end}'
|
|
self.window_count = window_count
|
|
self.fix_where_out_of_interval_range()
|
|
return self.where
|
|
|
|
def desc(self, sql: str) -> str:
|
|
if self.is_partition():
|
|
return sql + ' desc'
|
|
else:
|
|
return sql + ' order by _wstart desc'
|
|
|
|
def generate_extra(self):
|
|
if self.is_partition():
|
|
return 'order by tbname, _wstart'
|
|
else:
|
|
return ''
|
|
|
|
def no_fill_sql(self)-> str:
|
|
return f'select {self.generate_select_list()} from {self.dbname}.{self.tbname} WHERE {self.generate_where()} {self.generate_partition_by()} INTERVAL({self.generate_interval()}a) {self.generate_extra()}'
|
|
|
|
def generate_sql(self)-> str:
|
|
return f'select {self.generate_select_list()} FROM {self.dbname}.{self.tbname} WHERE {self.generate_where()} {self.generate_partition_by()} INTERVAL({self.generate_interval()}a) FILL({self.generate_fill()}) {self.generate_extra()}'
|
|
|
|
class FillResValidator:
|
|
def __init__(self, fill_res, interval_res, desc_res, generator: FillQueryGenerator):
|
|
self.fill_res = fill_res
|
|
self.interval_res = interval_res
|
|
self.desc_res = desc_res
|
|
self.generator = generator
|
|
|
|
def get_row(self, res, rowIdx):
|
|
if len(res) > rowIdx:
|
|
return res[rowIdx]
|
|
return None
|
|
|
|
def find_valid_val_for_col(self, res, fromRowIdx, forward: bool, colIdx):
|
|
if forward:
|
|
step = 1
|
|
end = len(res)
|
|
else:
|
|
step = -1
|
|
end = -1
|
|
for i in range(fromRowIdx, end, step):
|
|
if res[i][colIdx] is not None:
|
|
return res[i][colIdx]
|
|
return None
|
|
|
|
def find_last_valid_rows(self, res, colNum):
|
|
last_row = []
|
|
rowNum = len(res)
|
|
for colIdx in range(0, colNum):
|
|
last_row.append(None)
|
|
for rowIdx in range(rowNum-1, -1, -1):
|
|
if res[rowIdx][colIdx] is not None:
|
|
last_row[colIdx] = res[rowIdx][colIdx]
|
|
break
|
|
return last_row
|
|
|
|
def validate_fill_prev_one_group(self, fill_res_one_group, interval_res_one_group, desc_res_one_group: List):
|
|
if len(interval_res_one_group) == 0:
|
|
if len(fill_res_one_group) != 0:
|
|
tdLog.exit(f"interval got no res, fill should got not res too, but not: {len(fill_res_one_group)}")
|
|
return
|
|
if len(fill_res_one_group) != len(desc_res_one_group):
|
|
tdLog.exit(f"fill res: {len(fill_res_one_group)} desc res: {len(desc_res_one_group)}")
|
|
i = 0
|
|
j = 0
|
|
last_valid_row_val = self.find_last_valid_rows(interval_res_one_group, len(fill_res_one_group[0]))
|
|
desc_res_one_group.reverse()
|
|
while i < len(fill_res_one_group):
|
|
fill_row = fill_res_one_group[i]
|
|
desc_row = desc_res_one_group[i]
|
|
fill_ts = fill_row[0]
|
|
colNum = len(fill_row)
|
|
if self.generator.is_partition():
|
|
colNum -= 1
|
|
interval_row = self.get_row(interval_res_one_group, j)
|
|
if interval_row is None:
|
|
## from now on, all rows are generated by fill
|
|
for colIdx in range(1, colNum):
|
|
if fill_row[colIdx] != last_valid_row_val[colIdx]:
|
|
tdLog.exit(f"1 got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {last_valid_row_val[colIdx]} got: {fill_row}")
|
|
if desc_row[colIdx] != last_valid_row_val[colIdx]:
|
|
tdLog.exit(f"2 got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {last_valid_row_val[colIdx]} got: {desc_row}")
|
|
i += 1
|
|
else:
|
|
if fill_ts < interval_row[0]:
|
|
## this row is generated by fill
|
|
for colIdx in range(1, colNum):
|
|
val = self.find_valid_val_for_col(interval_res_one_group, j-1, False, colIdx)
|
|
if val != fill_row[colIdx]:
|
|
tdLog.exit(f"3 got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row}")
|
|
if val != desc_row[colIdx]:
|
|
tdLog.exit(f"4 got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {desc_row}")
|
|
i += 1
|
|
else:
|
|
## this row is copied from interval, but NULL cols are generated by fill
|
|
for colIdx in range(1, colNum):
|
|
filled = False
|
|
if interval_row[colIdx] is None:
|
|
val = self.find_valid_val_for_col(interval_res_one_group, j, False, colIdx)
|
|
filled = True
|
|
else:
|
|
val = interval_row[colIdx]
|
|
if val != fill_row[colIdx]:
|
|
tdLog.exit(f"5 got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row}")
|
|
if filled and val != desc_row[colIdx]:
|
|
tdLog.exit(f"6 got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {desc_row}")
|
|
i += 1
|
|
j += 1
|
|
|
|
def validate_fill_next_one_group(self, fill_res_one_group, interval_res_one_group, desc_res_one_group: List):
|
|
if len(interval_res_one_group) == 0:
|
|
if len(fill_res_one_group) != 0:
|
|
tdLog.exit(f"interval got no res, fill should got not res too, but not: {len(fill_res_one_group)}")
|
|
return
|
|
if len(fill_res_one_group) != len(desc_res_one_group):
|
|
tdLog.exit(f"fill res: {len(fill_res_one_group)} desc res: {len(desc_res_one_group)}")
|
|
i = 0
|
|
j = 0
|
|
desc_res_one_group.reverse()
|
|
while i < len(fill_res_one_group):
|
|
fill_row = fill_res_one_group[i]
|
|
desc_row = desc_res_one_group[i]
|
|
colNum = len(fill_row)
|
|
if self.generator.is_partition():
|
|
colNum -= 1
|
|
fill_ts = fill_row[0]
|
|
interval_row = self.get_row(interval_res_one_group, j)
|
|
if interval_row is None:
|
|
## from now on, all rows are generated by fill, fill next, so all should be None
|
|
for colIdx in range(1, colNum):
|
|
if fill_row[colIdx] != None:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: None got: {fill_row[colIdx]}")
|
|
if desc_row[colIdx] != None:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: None got: {desc_row[colIdx]}")
|
|
i += 1
|
|
else:
|
|
if fill_ts < interval_row[0]:
|
|
## this row is generated by fill
|
|
for colIdx in range(1, colNum):
|
|
val = self.find_valid_val_for_col(interval_res_one_group, j, True, colIdx)
|
|
if val != fill_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
if val != desc_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {desc_row[colIdx]}")
|
|
i += 1
|
|
else:
|
|
## this row is copied from interval, but NULL cols are generated by fill
|
|
for colIdx in range(1, colNum):
|
|
filled = False
|
|
if interval_row[colIdx] is None:
|
|
val = self.find_valid_val_for_col(interval_res_one_group, j + 1, True, colIdx)
|
|
filled = True
|
|
else:
|
|
val = interval_row[colIdx]
|
|
if val != fill_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
if filled and val != desc_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {desc_row[colIdx]}")
|
|
i += 1
|
|
j += 1
|
|
def get_one_group(self, res: List, rowIdx: int):
|
|
if len(res) <= rowIdx:
|
|
return None
|
|
val = res[rowIdx][-1]
|
|
if val is None and self.generator.fill != 'NULL_F':
|
|
tdLog.exit(f"tbname is None {res}")
|
|
for i in range(rowIdx+1, len(res)):
|
|
if val != res[i][-1]:
|
|
return (val, rowIdx, i)
|
|
return (val, rowIdx, len(res))
|
|
|
|
def split_groups(self, res: List)-> List:
|
|
output = []
|
|
idx = 0
|
|
while True:
|
|
tup = self.get_one_group(res, idx)
|
|
if tup is None:
|
|
break
|
|
else:
|
|
output.append(tup)
|
|
idx = tup[2]
|
|
return output
|
|
|
|
def split_res_groups(self)-> List:
|
|
res = []
|
|
fill_groups = self.split_groups(self.fill_res)
|
|
interval_groups = self.split_groups(self.interval_res)
|
|
desc_groups: List = self.split_groups(self.desc_res)
|
|
|
|
for i in range(0, len(fill_groups)):
|
|
fill_tup = fill_groups[i]
|
|
fill_rows = self.fill_res[fill_tup[1]: fill_tup[2]]
|
|
if len(interval_groups) > 0:
|
|
interval_tup = interval_groups[i]
|
|
interval_rows = self.interval_res[interval_tup[1]: interval_tup[2]]
|
|
else:
|
|
interval_tup = (None, 0, 0)
|
|
interval_rows = []
|
|
desc_tup = desc_groups[i]
|
|
desc_rows = self.desc_res[desc_tup[1]: desc_tup[2]]
|
|
res.append((fill_rows, interval_rows, desc_rows))
|
|
return res
|
|
|
|
def validate_fill_prev(self):
|
|
if not self.generator.is_partition():
|
|
return self.validate_fill_prev_one_group(self.fill_res, self.interval_res, self.desc_res)
|
|
else:
|
|
groups = self.split_res_groups()
|
|
for group in groups:
|
|
self.validate_fill_prev_one_group(group[0], group[1], group[2])
|
|
|
|
def validate_fill_next(self):
|
|
if not self.generator.is_partition():
|
|
return self.validate_fill_next_one_group(self.fill_res, self.interval_res, self.desc_res)
|
|
else:
|
|
groups = self.split_res_groups()
|
|
for group in groups:
|
|
self.validate_fill_next_one_group(group[0], group[1], group[2])
|
|
|
|
def validate_fill_null_rows(self, fill_res_one_group, interval_res_one_group, desc_res_one_group: List, null_f: bool):
|
|
if not null_f:
|
|
if len(interval_res_one_group) == 0:
|
|
if len(fill_res_one_group) != 0:
|
|
tdLog.exit(f"interval got no res, fill should got not res too, but not: {len(fill_res_one_group)}")
|
|
return
|
|
if len(fill_res_one_group) != len(desc_res_one_group):
|
|
tdLog.exit(f"fill res: {len(fill_res_one_group)} desc res: {len(desc_res_one_group)}")
|
|
return False
|
|
else:
|
|
if len(interval_res_one_group) == 0:
|
|
if len(fill_res_one_group) == 0:
|
|
tdLog.exit("fill null_f got not res")
|
|
for row in fill_res_one_group:
|
|
colIdx = len(row)
|
|
if self.generator.is_partition():
|
|
colIdx -= 1
|
|
for colIdx in range(1, colIdx):
|
|
if row[colIdx] != None:
|
|
tdLog.exit(f"got different val for fill_res NULL_F: {row} colIdx: {colIdx}, expect: None got: {row[colIdx]}")
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def validate_fill_null_one_group(self, fill_res_one_group, interval_res_one_group, desc_res_one_group, null_f: bool = False):
|
|
if self.validate_fill_null_rows(fill_res_one_group, interval_res_one_group, desc_res_one_group, null_f):
|
|
return
|
|
i = 0
|
|
j = 0
|
|
desc_res_one_group.reverse()
|
|
while i < len(fill_res_one_group):
|
|
fill_row = fill_res_one_group[i]
|
|
desc_row = desc_res_one_group[i]
|
|
colNum = len(fill_row)
|
|
if self.generator.is_partition():
|
|
colNum -= 1
|
|
fill_ts = fill_row[0]
|
|
interval_row = self.get_row(interval_res_one_group, j)
|
|
if interval_row is None:
|
|
## from now on, all rows are generated by fill, fill next, so all should be None
|
|
for colIdx in range(1, colNum):
|
|
if fill_row[colIdx] != None:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: None got: {fill_row[colIdx]}")
|
|
if fill_row[colIdx] != desc_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {desc_row[colIdx]} got: {fill_row[colIdx]}")
|
|
i += 1
|
|
else:
|
|
if fill_ts < interval_row[0]:
|
|
## this row is generated by fill
|
|
for colIdx in range(1, colNum):
|
|
val = None
|
|
if val != fill_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
if val != desc_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {desc_row[colIdx]} got: {fill_row[colIdx]}")
|
|
i += 1
|
|
else:
|
|
## this row is copied from interval
|
|
for colIdx in range(1, colNum):
|
|
val = interval_row[colIdx]
|
|
if val != fill_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
i += 1
|
|
j += 1
|
|
|
|
def validate_fill_NULL(self, null_f: bool = False):
|
|
if not self.generator.is_partition():
|
|
return self.validate_fill_null_one_group(self.fill_res, self.interval_res, self.desc_res, null_f)
|
|
else:
|
|
groups = self.split_res_groups()
|
|
for group in groups:
|
|
self.validate_fill_null_one_group(group[0], group[1], group[2], null_f)
|
|
|
|
def calc_linear_val(self, interval_res_one_group, rowIdx, colIdx, curTs: datetime):
|
|
prevRow = rowIdx - 1
|
|
if prevRow < 0:
|
|
return None
|
|
prevRowVal = interval_res_one_group[prevRow][colIdx]
|
|
curRowVal = interval_res_one_group[rowIdx][colIdx]
|
|
if prevRowVal is None or curRowVal is None:
|
|
return None
|
|
prevRowTs: datetime = interval_res_one_group[prevRow][0]
|
|
curRowTs:datetime = interval_res_one_group[rowIdx][0]
|
|
if curRowTs == prevRowTs:
|
|
raise Exception(f"{curRowTs} == {prevRowTs}")
|
|
else:
|
|
delta = (curRowVal - prevRowVal) / ((curRowTs - prevRowTs).total_seconds() * 1000)
|
|
result = prevRowVal + delta * (curTs - prevRowTs).total_seconds() * 1000
|
|
if isinstance(prevRowVal, int):
|
|
if math.isclose(result, math.ceil(result), rel_tol=1e-6, abs_tol=1e-6) or math.isclose(result, math.floor(result), rel_tol=1e-6, abs_tol=1e-6):
|
|
result = round(result)
|
|
else:
|
|
result = int(result)
|
|
#tdLog.debug(f"calc_linear_val: {prevRowVal} {curRowVal} {prevRowTs} {curRowTs} {delta} {result} deltats: {(curTs - prevRowTs).total_seconds() * 1000} {prevRowVal} + {delta} * {(curTs - prevRowTs).total_seconds() * 1000} = {result}")
|
|
return result
|
|
|
|
|
|
def validate_fill_linear_one_group(self, fill_res_one_group, interval_res_one_group, desc_res_one_group: List):
|
|
if len(interval_res_one_group) == 0:
|
|
if len(fill_res_one_group) != 0:
|
|
tdLog.exit(f"interval got no res, fill should got not res too, but not: {len(fill_res_one_group)}")
|
|
return
|
|
if len(fill_res_one_group) != len(desc_res_one_group):
|
|
tdLog.exit(f"fill res: {len(fill_res_one_group)} desc res: {len(desc_res_one_group)}")
|
|
i = 0
|
|
j = 0
|
|
desc_res_one_group.reverse()
|
|
while i < len(fill_res_one_group):
|
|
fill_row = fill_res_one_group[i]
|
|
desc_row = desc_res_one_group[i]
|
|
fill_ts = fill_row[0]
|
|
colNum = len(fill_row)
|
|
if self.generator.is_partition():
|
|
colNum -= 1
|
|
interval_row = self.get_row(interval_res_one_group, j)
|
|
if interval_row is None:
|
|
## from now on, all rows are generated by fill linear, since there is no data in interval output, all rows should be None
|
|
val = None
|
|
for colIdx in range(1, colNum):
|
|
if fill_row[colIdx] != val:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
if desc_row[colIdx] != val:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {desc_row[colIdx]}")
|
|
i += 1
|
|
else:
|
|
if fill_ts < interval_row[0]:
|
|
## this row is generated by fill
|
|
for colIdx in range(1, colNum):
|
|
|
|
val = self.calc_linear_val(interval_res_one_group, j, colIdx, fill_ts)
|
|
if val is None:
|
|
if fill_row[colIdx] is not None or desc_row[colIdx] is not None:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
else:
|
|
pass ## skip check linear val, only check None values
|
|
#if not math.isclose(val, fill_row[colIdx], rel_tol=1e-5, abs_tol=1e-6):
|
|
#tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
#if not math.isclose(val, desc_row[colIdx], rel_tol=1e-5, abs_tol=1e-6):
|
|
#tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {desc_row[colIdx]}")
|
|
i += 1
|
|
else:
|
|
## this row is copied from interval
|
|
for colIdx in range(1, colNum):
|
|
filled = False
|
|
val = interval_row[colIdx]
|
|
if val != fill_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {fill_row[colIdx]}")
|
|
if filled and val != desc_row[colIdx]:
|
|
tdLog.exit(f"got different val for fill_res: {fill_row} rowIdx: {i} colIdx: {colIdx}, expect: {val} got: {desc_row[colIdx]}")
|
|
i += 1
|
|
j += 1
|
|
|
|
def validate_fill_linear(self):
|
|
if not self.generator.is_partition():
|
|
return self.validate_fill_linear_one_group(self.fill_res, self.interval_res, self.desc_res)
|
|
else:
|
|
groups = self.split_res_groups()
|
|
for group in groups:
|
|
self.validate_fill_linear_one_group(group[0], group[1], group[2])
|
|
|
|
def validate(self):
|
|
if self.generator.fill == 'PREV':
|
|
return self.validate_fill_prev()
|
|
elif self.generator.fill == 'NEXT':
|
|
return self.validate_fill_next()
|
|
elif self.generator.fill == 'NULL':
|
|
return self.validate_fill_NULL()
|
|
elif self.generator.fill == 'LINEAR':
|
|
return self.validate_fill_linear()
|
|
elif self.generator.fill == 'NULL_F':
|
|
return self.validate_fill_NULL(True)
|
|
else:
|
|
return True
|
|
|
|
|
|
class TDTestCase:
|
|
updatecfgDict = {
|
|
"asynclog": 0,
|
|
"ttlUnit": 1,
|
|
"ttlPushInterval": 5,
|
|
"ratioOfVnodeStreamThrea": 4,
|
|
"debugFlag": 143,
|
|
}
|
|
|
|
def __init__(self):
|
|
self.vgroups = 4
|
|
self.ctbNum = 10
|
|
self.rowsPerTbl = 10000
|
|
self.duraion = "1h"
|
|
self.norm_tb_columns = []
|
|
self.tags = []
|
|
self.stable_name = "meters"
|
|
self.norm_table_name = "nt"
|
|
self.col_prefix = "c"
|
|
self.c_table_prefix = "t"
|
|
self.tag_name_prefix = "t"
|
|
self.db_name = "test"
|
|
self.c_table_num = 10
|
|
self.no_decimal_col_tb_name = "tt"
|
|
self.stb_columns = []
|
|
self.stream_name = "stream1"
|
|
self.stream_out_stb = "stream_out_stb"
|
|
self.tsma_name = "tsma1"
|
|
self.query_test_round = 10000
|
|
|
|
def init(self, conn, logSql, replicaVar=1):
|
|
self.replicaVar = int(replicaVar)
|
|
tdLog.debug(f"start to excute {__file__}")
|
|
tdSql.init(conn.cursor(), False)
|
|
|
|
def test_decimal_column_ddl(self):
|
|
## create decimal type table, normal/super table, decimal64/decimal128
|
|
tdLog.printNoPrefix("-------- test create columns")
|
|
self.norm_tb_columns: List[Column] = [
|
|
Column(DecimalType(TypeEnum.DECIMAL, 10, 2)),
|
|
Column(DecimalType(TypeEnum.DECIMAL, 20, 4)),
|
|
Column(DecimalType(TypeEnum.DECIMAL, 30, 8)),
|
|
Column(DecimalType(TypeEnum.DECIMAL, 38, 10)),
|
|
Column(DataType(TypeEnum.TINYINT)),
|
|
Column(DataType(TypeEnum.INT)),
|
|
Column(DataType(TypeEnum.BIGINT)),
|
|
Column(DataType(TypeEnum.DOUBLE)),
|
|
Column(DataType(TypeEnum.FLOAT)),
|
|
Column(DataType(TypeEnum.VARCHAR, 255)),
|
|
]
|
|
self.tags: List[Column] = [
|
|
Column(DataType(TypeEnum.INT)),
|
|
Column(DataType(TypeEnum.VARCHAR, 255)),
|
|
]
|
|
self.stb_columns: List[Column] = [
|
|
Column(DecimalType(TypeEnum.DECIMAL, 10, 2)),
|
|
Column(DecimalType(TypeEnum.DECIMAL, 20, 4)),
|
|
Column(DecimalType(TypeEnum.DECIMAL, 30, 8)),
|
|
Column(DecimalType(TypeEnum.DECIMAL, 38, 10)),
|
|
Column(DataType(TypeEnum.TINYINT)),
|
|
Column(DataType(TypeEnum.INT)),
|
|
Column(DataType(TypeEnum.BIGINT)),
|
|
Column(DataType(TypeEnum.DOUBLE)),
|
|
Column(DataType(TypeEnum.FLOAT)),
|
|
Column(DataType(TypeEnum.VARCHAR, 255)),
|
|
]
|
|
DecimalColumnTableCreater(
|
|
tdSql,
|
|
self.db_name,
|
|
self.stable_name,
|
|
self.stb_columns,
|
|
self.tags,
|
|
col_prefix=self.col_prefix,
|
|
tag_prefix=self.tag_name_prefix,
|
|
).create()
|
|
|
|
DecimalColumnTableCreater(
|
|
tdSql, self.db_name, self.norm_table_name, self.norm_tb_columns
|
|
).create()
|
|
|
|
## TODO add more values for all rows
|
|
tag_values = ["1", "t1"]
|
|
DecimalColumnTableCreater(
|
|
tdSql, self.db_name, self.stable_name, self.stb_columns
|
|
).create_child_table(
|
|
self.c_table_prefix, self.c_table_num, self.tags, tag_values
|
|
)
|
|
|
|
def test_insert_decimal_values(self):
|
|
tdLog.debug("start to insert values")
|
|
for i in range(self.c_table_num):
|
|
TableInserter(
|
|
tdSql,
|
|
self.db_name,
|
|
f"{self.c_table_prefix}{i}",
|
|
self.stb_columns,
|
|
self.tags,
|
|
).insert(tb_insert_rows, 1537146000000, 5000)
|
|
|
|
TableInserter(
|
|
tdSql, self.db_name, self.norm_table_name, self.norm_tb_columns
|
|
).insert(tb_insert_rows, 1537146000000, 5000, flush_database=True)
|
|
tdSql.execute("flush database %s" % (self.db_name), queryTimes=1)
|
|
|
|
def test_decimal_ddl(self):
|
|
tdSql.execute("create database test cachemodel 'both'", queryTimes=1)
|
|
self.test_decimal_column_ddl()
|
|
|
|
def run(self):
|
|
self.test_decimal_ddl()
|
|
self.test_insert_decimal_values()
|
|
self.test_fill(self.db_name, self.norm_table_name, self.norm_tb_columns)
|
|
self.test_fill(self.db_name, self.stable_name, self.stb_columns)
|
|
|
|
def get_first_last_ts(self, dbname, tbname):
|
|
sql = f'select cast(first(ts) as bigint), cast(last(ts) as bigint) from {dbname}.{tbname}'
|
|
tdSql.query(sql, queryTimes=1)
|
|
first: int = tdSql.queryResult[0][0]
|
|
last: int = tdSql.queryResult[0][1]
|
|
return (first, last)
|
|
|
|
def test_fill(self, dbname, tbname, cols):
|
|
(first, last) = self.get_first_last_ts(dbname, tbname)
|
|
for _ in range(test_round):
|
|
sql_generator = FillQueryGenerator(dbname, tbname, cols, first, last)
|
|
sql = sql_generator.generate_sql()
|
|
interval_sql = sql_generator.no_fill_sql()
|
|
tdSql.query(interval_sql, queryTimes=1)
|
|
interval_res = tdSql.queryResult
|
|
tdLog.debug(sql)
|
|
tdSql.query(sql, queryTimes=1)
|
|
fill_res = tdSql.queryResult
|
|
desc_sql = sql_generator.desc(sql)
|
|
tdSql.query(desc_sql, queryTimes=1)
|
|
desc_res = tdSql.queryResult
|
|
FillResValidator(fill_res, interval_res, desc_res, sql_generator).validate()
|
|
tdLog.debug(f"validate fill res for {sql} success got fill rows: {len(fill_res)}")
|
|
|
|
def stop(self):
|
|
tdSql.close()
|
|
tdLog.success(f"{__file__} successfully executed")
|
|
|
|
def wait_query_result(self, sql: str, expect_result, times):
|
|
for i in range(times):
|
|
tdLog.info(f"wait query result for {sql}, times: {i}")
|
|
tdSql.query(sql, queryTimes=1)
|
|
results = tdSql.queryResult
|
|
if results != expect_result:
|
|
time.sleep(1)
|
|
continue
|
|
return True
|
|
tdLog.exit(
|
|
f"wait query result timeout for {sql} failed after {times} time, expect {expect_result}, but got {results}"
|
|
)
|
|
|
|
def wait_query_at_least_rows(self, sql: str, rows, wait_times):
|
|
for i in range(wait_times):
|
|
tdLog.info(f"wait query rows at least for {sql}, times: {i}")
|
|
tdSql.query(sql, queryTimes=1, show=True)
|
|
results = tdSql.queryResult
|
|
if len(results) < rows:
|
|
time.sleep(1)
|
|
continue
|
|
return True
|
|
tdLog.exit(
|
|
f"wait query rows at least for {sql} failed after {wait_times} times, expect at least {rows} rows, but got {len(results)} rows"
|
|
)
|
|
|
|
def run_in_thread(self, times, func, params) -> threading.Thread:
|
|
threads: List[threading.Thread] = []
|
|
for i in range(times):
|
|
t = threading.Thread(target=func, args=params)
|
|
t.start()
|
|
threads.append(t)
|
|
for t in threads:
|
|
t.join()
|
|
|
|
def run_in_thread2(self, func, params) -> threading.Thread:
|
|
t = threading.Thread(target=func, args=params)
|
|
t.start()
|
|
return t
|
|
|
|
def test_query_decimal_interval_fill(self):
|
|
pass
|
|
|
|
event = threading.Event()
|
|
|
|
tdCases.addLinux(__file__, TDTestCase())
|
|
tdCases.addWindows(__file__, TDTestCase())
|