misc.python.materialize.sqlancer
Shared code for running SQLancer and SQLancer++ against Materialize.
1# Copyright Materialize, Inc. and contributors. All rights reserved. 2# 3# Use of this software is governed by the Business Source License 4# included in the LICENSE file at the root of this repository. 5# 6# As of the Change Date specified in that file, in accordance with 7# the Business Source License, use of this software will be governed 8# by the Apache License, Version 2.0. 9 10""" 11Shared code for running SQLancer and SQLancer++ against Materialize. 12""" 13 14import argparse 15import random 16import re 17import shutil 18import subprocess 19from collections.abc import Callable 20from pathlib import Path 21from threading import Thread 22 23from materialize import buildkite, spawn 24from materialize.mzcompose.composition import ( 25 Composition, 26 WorkflowArgumentParser, 27) 28from materialize.mzcompose.service import Service 29from materialize.mzcompose.services.materialized import Materialized 30 31IGNORED_ERROR_PATTERNS = [ 32 r"^(?!.*panic).*not yet supported", 33 r"does not exist", 34 r"must have", 35 r"overflow", 36 r"invalid input", 37 r"cannot be matched", 38 r"implicitly casting", 39 r"is not unique", 40 r"invalid digit", 41 r"is not defined", 42 r"is defined", 43 r"unterminated", 44 r"Expected ", 45 r"is out of range", 46 r"of a negative", 47 r"does not support", 48 r"ANALYZE", 49 r"is of type", 50 r"must appear in the", 51 r"negative substring length", 52 r"is ambiguous", 53 r"out of range", 54 r"division by zero", 55 r"is only defined for finite", 56 r"cannot canonicalize predicates that are not of type", 57 r"unexpected character in input", 58 r"could not determine polymorphic type", 59 r"cannot reference pseudo type", 60 r"string is not a valid identifier", 61 r"invalid regular expression", 62 r" violates not-null constraint", 63 r"could not convert type", 64 r"are not allowed", 65 r"requires a record", 66 r"unrecognized privilege type", 67 r"cannot cast", 68 r"requires an OVER", 69 r"value too long", 70 r"may only refer to user-defined", 71 r"cannot materialize call to", 72 r"invalid hash algorithm", 73 r"expected id", 74 r"expected exactly one statement", 75 r"regex parse error", 76 r"invalid encoding name", 77 r"must specify at least one capture group", 78 r"requires a string literal", 79 r"too large for encoding", 80 r"must be a positive integer", 81 r"bound must be less than", 82 r"not recognized", 83 r"invalid escape string", 84 r"canceling statement due to statement timeout", 85 r"invalid time zone", 86 r"result exceeds max size", 87 r"calls to mz_now in write statements", 88 r"out of valid range", 89 r"more than one record produced in subquery", 90 r"input of anonymous composite types is not implemented", 91 r"lists must all be the same length", 92 r"invalid IANA Time Zone Database identifier", 93 r"unknown schema", 94 r"octal escapes are not supported", 95 r"attempt to create relation with too many columns", 96 r"column notation applied to type text", 97 r"Unexpected EOF", 98 r"missing required exponent", 99 r"invalid unicode escape", 100 r"dimension array or low bound array must not be null", 101 r"dimension values must not be null", 102 r"LIKE pattern exceeds maximum length", 103 r"cannot return complex numbers", 104 r"must be greater than zero", 105 r"null character not permitted", 106 r"invalid datepart", 107 r"must use value within", 108 r"null character not permitted", 109 r"expressions must appear in select list", 110 r"expressions must match initial", 111 r"invalid selection: operation may only", 112 r"array size exceeds the maximum allowed", 113 r"does not allow subqueries", 114 r"must use value within", 115 r"zero raised to a negative power is undefined", 116 r"range type over", 117 r"Evaluation error: Unexpected keyword .* at the beginning of a statement", 118] 119 120 121def check_query_errors(logs_dir: Path) -> list[str]: 122 unexpected_errors = [] 123 log_files = list(logs_dir.glob("*.log")) 124 125 for log_file in log_files: 126 try: 127 content = log_file.read_text() 128 except Exception: 129 continue 130 131 for line in content.splitlines(): 132 if "ERROR" not in line: 133 continue 134 135 is_ignored = False 136 for pattern in IGNORED_ERROR_PATTERNS: 137 if re.search(pattern, line): 138 is_ignored = True 139 break 140 141 if not is_ignored: 142 unexpected_errors.append(f"{log_file.name}: {line}") 143 144 return unexpected_errors 145 146 147def create_services(service_name: str) -> list[Service | Materialized]: 148 """Create the services needed for SQLancer or SQLancer++ tests.""" 149 return [ 150 # Auto-restart so we can keep testing even after we ran into a panic 151 Materialized( 152 restart="on-failure", 153 default_replication_factor=1, 154 additional_system_parameter_defaults={ 155 "enable_alter_table_add_column": "true", 156 "enable_statement_lifecycle_logging": "false", 157 "enable_internal_statement_logging": "false", 158 "statement_logging_default_sample_rate": "0", 159 "statement_logging_max_sample_rate": "0", 160 "enable_repeat_row": "true", 161 "enable_list_length_max": "true", 162 "enable_list_n_layers": "true", 163 "enable_time_at_time_zone": "true", 164 "enable_date_bin_hopping": "true", 165 }, 166 ), 167 Service( 168 service_name, 169 { 170 "mzbuild": service_name, 171 }, 172 ), 173 ] 174 175 176def _print_logs(container_id: str) -> None: 177 spawn.runv(["docker", "logs", "-f", container_id]) 178 179 180def run_sqlancer( 181 c: Composition, 182 parser: WorkflowArgumentParser, 183 *, 184 service_name: str, 185 default_oracle: str, 186 build_run_args: Callable[[argparse.Namespace, int], list[str]], 187 docker_logs_path: str, 188 log_prefix: str, 189) -> None: 190 parser.add_argument("--runtime", default=600, type=int) 191 parser.add_argument("--num-tries", default=100000, type=int) 192 parser.add_argument("--num-threads", default=16, type=int) 193 parser.add_argument("--seed", default=None, type=int) 194 parser.add_argument("--qpg", default=True, action=argparse.BooleanOptionalAction) 195 parser.add_argument("--oracle", default=default_oracle, type=str) 196 args = parser.parse_args() 197 198 c.up("materialized") 199 200 c.sql( 201 "ALTER SYSTEM SET max_databases TO 1000", 202 user="mz_system", 203 port=6877, 204 ) 205 c.sql( 206 "ALTER SYSTEM SET max_tables TO 1000", 207 user="mz_system", 208 port=6877, 209 ) 210 c.sql( 211 "ALTER SYSTEM SET max_materialized_views TO 1000", 212 user="mz_system", 213 port=6877, 214 ) 215 216 seed = args.seed or random.randint(0, 2**31) 217 218 run_args = build_run_args(args, seed) 219 220 print("--- Run in progress") 221 result = c.run( 222 service_name, 223 *run_args, 224 check=False, 225 detach=True, 226 capture=True, 227 ) 228 container_id = result.stdout.strip() 229 230 # Print logs in a background thread so that we get immediate output in CI, 231 # and also when running SQLancer locally 232 thread = Thread(target=_print_logs, args=(container_id,)) 233 thread.start() 234 # At the same time capture the logs to analyze for finding new issues 235 stdout = spawn.capture( 236 ["docker", "logs", "-f", container_id], stderr=subprocess.STDOUT 237 ) 238 239 in_assertion = False 240 for line in stdout.splitlines(): 241 if "OutOfMemoryError" in line or "IgnoreMeException" in line: 242 continue 243 if line.startswith("--java.lang."): 244 in_assertion = True 245 print(f"--- [{log_prefix}] {line.removeprefix('--java.lang.')}") 246 elif line == "": 247 in_assertion = False 248 elif in_assertion: 249 print(line) 250 print(f"--- {result.stdout.splitlines()[-1]}") 251 252 # Check for unexpected query errors in the logs 253 logs = Path("logs") 254 if logs.exists() and logs.is_dir(): 255 shutil.rmtree(logs) 256 spawn.runv( 257 [ 258 "docker", 259 "cp", 260 f"{container_id}:{docker_logs_path}", 261 str(logs), 262 ] 263 ) 264 spawn.runv(["tar", "cfz", "logs.tar.gz", str(logs)]) 265 buildkite.upload_artifact("logs.tar.gz") 266 unexpected_errors = check_query_errors(logs) 267 if unexpected_errors: 268 print("--- Unexpected query errors found:") 269 for error in unexpected_errors: 270 print(error) 271 raise Exception( 272 f"Found {len(unexpected_errors)} unexpected query error(s) in logs" 273 )
IGNORED_ERROR_PATTERNS =
['^(?!.*panic).*not yet supported', 'does not exist', 'must have', 'overflow', 'invalid input', 'cannot be matched', 'implicitly casting', 'is not unique', 'invalid digit', 'is not defined', 'is defined', 'unterminated', 'Expected ', 'is out of range', 'of a negative', 'does not support', 'ANALYZE', 'is of type', 'must appear in the', 'negative substring length', 'is ambiguous', 'out of range', 'division by zero', 'is only defined for finite', 'cannot canonicalize predicates that are not of type', 'unexpected character in input', 'could not determine polymorphic type', 'cannot reference pseudo type', 'string is not a valid identifier', 'invalid regular expression', ' violates not-null constraint', 'could not convert type', 'are not allowed', 'requires a record', 'unrecognized privilege type', 'cannot cast', 'requires an OVER', 'value too long', 'may only refer to user-defined', 'cannot materialize call to', 'invalid hash algorithm', 'expected id', 'expected exactly one statement', 'regex parse error', 'invalid encoding name', 'must specify at least one capture group', 'requires a string literal', 'too large for encoding', 'must be a positive integer', 'bound must be less than', 'not recognized', 'invalid escape string', 'canceling statement due to statement timeout', 'invalid time zone', 'result exceeds max size', 'calls to mz_now in write statements', 'out of valid range', 'more than one record produced in subquery', 'input of anonymous composite types is not implemented', 'lists must all be the same length', 'invalid IANA Time Zone Database identifier', 'unknown schema', 'octal escapes are not supported', 'attempt to create relation with too many columns', 'column notation applied to type text', 'Unexpected EOF', 'missing required exponent', 'invalid unicode escape', 'dimension array or low bound array must not be null', 'dimension values must not be null', 'LIKE pattern exceeds maximum length', 'cannot return complex numbers', 'must be greater than zero', 'null character not permitted', 'invalid datepart', 'must use value within', 'null character not permitted', 'expressions must appear in select list', 'expressions must match initial', 'invalid selection: operation may only', 'array size exceeds the maximum allowed', 'does not allow subqueries', 'must use value within', 'zero raised to a negative power is undefined', 'range type over', 'Evaluation error: Unexpected keyword .* at the beginning of a statement']
def
check_query_errors(logs_dir: pathlib._local.Path) -> list[str]:
122def check_query_errors(logs_dir: Path) -> list[str]: 123 unexpected_errors = [] 124 log_files = list(logs_dir.glob("*.log")) 125 126 for log_file in log_files: 127 try: 128 content = log_file.read_text() 129 except Exception: 130 continue 131 132 for line in content.splitlines(): 133 if "ERROR" not in line: 134 continue 135 136 is_ignored = False 137 for pattern in IGNORED_ERROR_PATTERNS: 138 if re.search(pattern, line): 139 is_ignored = True 140 break 141 142 if not is_ignored: 143 unexpected_errors.append(f"{log_file.name}: {line}") 144 145 return unexpected_errors
def
create_services( service_name: str) -> list[materialize.mzcompose.service.Service | materialize.mzcompose.services.materialized.Materialized]:
148def create_services(service_name: str) -> list[Service | Materialized]: 149 """Create the services needed for SQLancer or SQLancer++ tests.""" 150 return [ 151 # Auto-restart so we can keep testing even after we ran into a panic 152 Materialized( 153 restart="on-failure", 154 default_replication_factor=1, 155 additional_system_parameter_defaults={ 156 "enable_alter_table_add_column": "true", 157 "enable_statement_lifecycle_logging": "false", 158 "enable_internal_statement_logging": "false", 159 "statement_logging_default_sample_rate": "0", 160 "statement_logging_max_sample_rate": "0", 161 "enable_repeat_row": "true", 162 "enable_list_length_max": "true", 163 "enable_list_n_layers": "true", 164 "enable_time_at_time_zone": "true", 165 "enable_date_bin_hopping": "true", 166 }, 167 ), 168 Service( 169 service_name, 170 { 171 "mzbuild": service_name, 172 }, 173 ), 174 ]
Create the services needed for SQLancer or SQLancer++ tests.
def
run_sqlancer( c: materialize.mzcompose.composition.Composition, parser: materialize.mzcompose.composition.WorkflowArgumentParser, *, service_name: str, default_oracle: str, build_run_args: Callable[[argparse.Namespace, int], list[str]], docker_logs_path: str, log_prefix: str) -> None:
181def run_sqlancer( 182 c: Composition, 183 parser: WorkflowArgumentParser, 184 *, 185 service_name: str, 186 default_oracle: str, 187 build_run_args: Callable[[argparse.Namespace, int], list[str]], 188 docker_logs_path: str, 189 log_prefix: str, 190) -> None: 191 parser.add_argument("--runtime", default=600, type=int) 192 parser.add_argument("--num-tries", default=100000, type=int) 193 parser.add_argument("--num-threads", default=16, type=int) 194 parser.add_argument("--seed", default=None, type=int) 195 parser.add_argument("--qpg", default=True, action=argparse.BooleanOptionalAction) 196 parser.add_argument("--oracle", default=default_oracle, type=str) 197 args = parser.parse_args() 198 199 c.up("materialized") 200 201 c.sql( 202 "ALTER SYSTEM SET max_databases TO 1000", 203 user="mz_system", 204 port=6877, 205 ) 206 c.sql( 207 "ALTER SYSTEM SET max_tables TO 1000", 208 user="mz_system", 209 port=6877, 210 ) 211 c.sql( 212 "ALTER SYSTEM SET max_materialized_views TO 1000", 213 user="mz_system", 214 port=6877, 215 ) 216 217 seed = args.seed or random.randint(0, 2**31) 218 219 run_args = build_run_args(args, seed) 220 221 print("--- Run in progress") 222 result = c.run( 223 service_name, 224 *run_args, 225 check=False, 226 detach=True, 227 capture=True, 228 ) 229 container_id = result.stdout.strip() 230 231 # Print logs in a background thread so that we get immediate output in CI, 232 # and also when running SQLancer locally 233 thread = Thread(target=_print_logs, args=(container_id,)) 234 thread.start() 235 # At the same time capture the logs to analyze for finding new issues 236 stdout = spawn.capture( 237 ["docker", "logs", "-f", container_id], stderr=subprocess.STDOUT 238 ) 239 240 in_assertion = False 241 for line in stdout.splitlines(): 242 if "OutOfMemoryError" in line or "IgnoreMeException" in line: 243 continue 244 if line.startswith("--java.lang."): 245 in_assertion = True 246 print(f"--- [{log_prefix}] {line.removeprefix('--java.lang.')}") 247 elif line == "": 248 in_assertion = False 249 elif in_assertion: 250 print(line) 251 print(f"--- {result.stdout.splitlines()[-1]}") 252 253 # Check for unexpected query errors in the logs 254 logs = Path("logs") 255 if logs.exists() and logs.is_dir(): 256 shutil.rmtree(logs) 257 spawn.runv( 258 [ 259 "docker", 260 "cp", 261 f"{container_id}:{docker_logs_path}", 262 str(logs), 263 ] 264 ) 265 spawn.runv(["tar", "cfz", "logs.tar.gz", str(logs)]) 266 buildkite.upload_artifact("logs.tar.gz") 267 unexpected_errors = check_query_errors(logs) 268 if unexpected_errors: 269 print("--- Unexpected query errors found:") 270 for error in unexpected_errors: 271 print(error) 272 raise Exception( 273 f"Found {len(unexpected_errors)} unexpected query error(s) in logs" 274 )