master
sh 1,530 lines 43.7 KB
Raw
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5 # shellcheck disable=SC1091
6 . "$SCRIPT_DIR/lib.sh"
7
8 init_workspace "mssql"
9 trap cleanup EXIT
10
11 MSSQL_PORT="$(reserve_port)"
12 write_env "MSSQL_PORT" "$MSSQL_PORT"
13 MSSQL_CONF="$WORKDIR/config/go.d/mssql.conf"
14 replace_in_file "$MSSQL_CONF" "127.0.0.1:1433" "127.0.0.1:${MSSQL_PORT}"
15
16 MSSQL_VARIANT_LABEL="${MSSQL_VARIANT:-mssql}"
17 if [ -n "${MSSQL_IMAGE:-}" ]; then
18 write_env "MSSQL_IMAGE" "$MSSQL_IMAGE"
19 fi
20
21 compose_up mssql
22 wait_healthy mssql 120
23 compose_run mssql-init
24
25 build_plugin
26
27 MSSQL_JOB_RETRIES="${MSSQL_JOB_RETRIES:-6}"
28 MSSQL_JOB_RETRY_DELAY="${MSSQL_JOB_RETRY_DELAY:-5}"
29
30 mssql_is_no_jobs_started() {
31 local input="$1"
32 if [ ! -s "$input" ]; then
33 return 1
34 fi
35
36 if command -v python3 >/dev/null 2>&1; then
37 python3 - "$input" <<'PY'
38 import json
39 import sys
40
41 path = sys.argv[1]
42 try:
43 with open(path, "r", encoding="utf-8") as fh:
44 doc = json.load(fh)
45 except Exception:
46 raise SystemExit(1)
47
48 status = doc.get("status")
49 msg = str(doc.get("errorMessage") or "").lower()
50 if status == 503 and "no jobs started for module" in msg:
51 raise SystemExit(0)
52 raise SystemExit(1)
53 PY
54 return $?
55 fi
56
57 python - "$input" <<'PY'
58 import json
59 import sys
60
61 path = sys.argv[1]
62 try:
63 with open(path, "r") as fh:
64 doc = json.load(fh)
65 except Exception:
66 raise SystemExit(1)
67
68 status = doc.get("status")
69 msg = str(doc.get("errorMessage") or "").lower()
70 if status == 503 and "no jobs started for module" in msg:
71 raise SystemExit(0)
72 raise SystemExit(1)
73 PY
74 }
75
76 run_mssql_info_with_retry() {
77 local output="$WORKDIR/mssql-top-queries-info.json"
78 local attempt=1
79
80 while true; do
81 if run "$WORKDIR/go.d.plugin" \
82 --config-dir "$WORKDIR/config" \
83 --function "mssql:top-queries" \
84 --function-args info \
85 > "$output"; then
86 validate "$output"
87 return 0
88 fi
89
90 if mssql_is_no_jobs_started "$output"; then
91 if [ "$attempt" -ge "$MSSQL_JOB_RETRIES" ]; then
92 echo "Timed out waiting for mssql jobs to start (info)" >&2
93 return 1
94 fi
95 attempt=$((attempt + 1))
96 sleep "$MSSQL_JOB_RETRY_DELAY"
97 continue
98 fi
99
100 echo "Unexpected failure while running mssql top-queries info" >&2
101 cat "$output" >&2
102 return 1
103 done
104 }
105
106 run_mssql_top_queries_with_retry() {
107 local output="$WORKDIR/mssql-top-queries.json"
108 local attempt=1
109
110 while true; do
111 if run "$WORKDIR/go.d.plugin" \
112 --config-dir "$WORKDIR/config" \
113 --function "mssql:top-queries" \
114 --function-args __job:local \
115 > "$output"; then
116 validate "$output" --min-rows 1
117 return 0
118 fi
119
120 if mssql_is_no_jobs_started "$output"; then
121 if [ "$attempt" -ge "$MSSQL_JOB_RETRIES" ]; then
122 echo "Timed out waiting for mssql jobs to start (top-queries)" >&2
123 return 1
124 fi
125 attempt=$((attempt + 1))
126 sleep "$MSSQL_JOB_RETRY_DELAY"
127 continue
128 fi
129
130 echo "Unexpected failure while running mssql top-queries" >&2
131 cat "$output" >&2
132 return 1
133 done
134 }
135
136 run_mssql_function_with_retry() {
137 local method="$1"
138 local args="${2:-__job:local}"
139 local require_rows="${3:-true}"
140 local output="$WORKDIR/mssql-${method}.json"
141 local attempt=1
142
143 while true; do
144 if run "$WORKDIR/go.d.plugin" \
145 --config-dir "$WORKDIR/config" \
146 --function "mssql:${method}" \
147 --function-args "$args" \
148 > "$output"; then
149 if [ "$require_rows" = "true" ]; then
150 validate "$output" --min-rows 1
151 else
152 validate "$output"
153 fi
154 echo "$output"
155 return 0
156 fi
157
158 if mssql_is_no_jobs_started "$output"; then
159 if [ "$attempt" -ge "$MSSQL_JOB_RETRIES" ]; then
160 echo "Timed out waiting for mssql jobs to start (${method})" >&2
161 return 1
162 fi
163 attempt=$((attempt + 1))
164 sleep "$MSSQL_JOB_RETRY_DELAY"
165 continue
166 fi
167
168 echo "Unexpected failure while running mssql ${method}" >&2
169 cat "$output" >&2
170 return 1
171 done
172 }
173
174 run_mssql_info_with_retry
175 run_mssql_top_queries_with_retry
176
177 mssql_container_id() {
178 "${COMPOSE[@]}" ps -q mssql
179 }
180
181 mssql_sqlcmd_path() {
182 local cid
183 cid="$(mssql_container_id)"
184 if [ -z "$cid" ]; then
185 echo "MSSQL container ID not found" >&2
186 return 1
187 fi
188 docker exec -i "$cid" bash -lc 'if [ -x /opt/mssql-tools18/bin/sqlcmd ]; then echo /opt/mssql-tools18/bin/sqlcmd; elif [ -x /opt/mssql-tools/bin/sqlcmd ]; then echo /opt/mssql-tools/bin/sqlcmd; else exit 1; fi'
189 }
190
191 MSSQL_SQLCMD_PATH="$(mssql_sqlcmd_path)"
192 echo "Using sqlcmd path: $MSSQL_SQLCMD_PATH" >&2
193
194 mssql_sqlcmd_supports_c() {
195 local cid
196 cid="$(mssql_container_id)"
197 if [ -z "$cid" ]; then
198 return 1
199 fi
200 set +e
201 local help
202 help="$(docker exec -i "$cid" "$MSSQL_SQLCMD_PATH" -? 2>&1)"
203 local status=$?
204 set -e
205 if [ $status -ne 0 ] && [ -z "$help" ]; then
206 return 1
207 fi
208 echo "$help" | grep -q " -C"
209 }
210
211 MSSQL_SQLCMD_CFLAG=()
212 case "$MSSQL_SQLCMD_PATH" in
213 *mssql-tools18*)
214 MSSQL_SQLCMD_CFLAG=(-C)
215 ;;
216 *)
217 if mssql_sqlcmd_supports_c; then
218 MSSQL_SQLCMD_CFLAG=(-C)
219 fi
220 ;;
221 esac
222
223 mssql_exec_sa() {
224 local sql="$1"
225 local cid
226 cid="$(mssql_container_id)"
227 if [ -z "$cid" ]; then
228 echo "MSSQL container ID not found" >&2
229 return 1
230 fi
231 run docker exec -i "$cid" "$MSSQL_SQLCMD_PATH" "${MSSQL_SQLCMD_CFLAG[@]}" -S localhost -U sa -P "Netdata123!" -d netdata -b -y 0 -Y 0 -Q "$sql"
232 }
233
234 mssql_exec_sa_allow_error() {
235 local sql="$1"
236 local cid
237 cid="$(mssql_container_id)"
238 if [ -z "$cid" ]; then
239 echo "MSSQL container ID not found" >&2
240 return 1
241 fi
242 set +e
243 docker exec -i "$cid" "$MSSQL_SQLCMD_PATH" "${MSSQL_SQLCMD_CFLAG[@]}" -S localhost -U sa -P "Netdata123!" -d netdata -b -y 0 -Y 0 -Q "$sql" >/dev/null 2>&1
244 set -e
245 }
246
247 induce_deadlock_once() {
248 local tx1
249 local tx2
250
251 tx1="$(cat <<'SQL'
252 SET NOCOUNT ON;
253 SET LOCK_TIMEOUT 5000;
254 BEGIN TRAN;
255 UPDATE dbo.deadlock_a SET value = value + 1 WHERE id = 1;
256 WAITFOR DELAY '00:00:01';
257 UPDATE dbo.deadlock_b SET value = value + 1 WHERE id = 1;
258 COMMIT;
259 SQL
260 )"
261
262 tx2="$(cat <<'SQL'
263 SET NOCOUNT ON;
264 SET LOCK_TIMEOUT 5000;
265 BEGIN TRAN;
266 UPDATE dbo.deadlock_b SET value = value + 1 WHERE id = 1;
267 WAITFOR DELAY '00:00:01';
268 UPDATE dbo.deadlock_a SET value = value + 1 WHERE id = 1;
269 COMMIT;
270 SQL
271 )"
272
273 mssql_exec_sa "$tx1" &
274 local pid1=$!
275 mssql_exec_sa "$tx2" &
276 local pid2=$!
277
278 set +e
279 wait "$pid1"
280 wait "$pid2"
281 set -e
282 }
283
284 assert_deadlock_info_content() {
285 local input="$1"
286 if command -v python3 >/dev/null 2>&1; then
287 python3 - "$input" <<'PY'
288 import json
289 import re
290 import sys
291
292 path = sys.argv[1]
293 with open(path, "r", encoding="utf-8") as fh:
294 doc = json.load(fh)
295
296 try:
297 status = int(doc.get("status"))
298 except (TypeError, ValueError):
299 raise SystemExit(f"unexpected status value: {doc.get('status')!r}")
300
301 if status != 200:
302 raise SystemExit(f"expected status 200, got {status}")
303
304 if doc.get("errorMessage"):
305 raise SystemExit(f"unexpected errorMessage on status 200: {doc.get('errorMessage')!r}")
306
307 columns = doc.get("columns") or {}
308 field_to_idx = {}
309 if isinstance(columns, dict):
310 for field, col in columns.items():
311 if not isinstance(col, dict):
312 continue
313 try:
314 field_to_idx[field] = int(col.get("index"))
315 except (TypeError, ValueError):
316 continue
317 else:
318 for idx, col in enumerate(columns):
319 if not isinstance(col, dict):
320 continue
321 field = col.get("field")
322 if field:
323 field_to_idx[field] = idx
324
325 for required in ("row_id", "deadlock_id", "process_id", "is_victim", "lock_mode", "lock_status", "query_text", "wait_resource", "database"):
326 if required not in field_to_idx:
327 raise SystemExit(f"missing expected column: {required}")
328
329 data = doc.get("data") or []
330 if not data:
331 raise SystemExit("deadlock-info returned no rows")
332
333 def get_value(row, field):
334 idx = field_to_idx[field]
335 return row[idx] if idx < len(row) else None
336
337 def norm(val):
338 return "" if val is None else str(val).strip()
339
340 has_waiting = any(str(get_value(row, "lock_status")).upper() == "WAITING" for row in data)
341 if not has_waiting:
342 raise SystemExit("no WAITING lock_status found in deadlock-info output")
343
344 table_pattern = re.compile(r"deadlock_(a|b)", re.IGNORECASE)
345 has_expected_query = any(table_pattern.search(str(get_value(row, "query_text"))) for row in data)
346 if not has_expected_query:
347 raise SystemExit("query_text does not reference deadlock tables")
348
349 waiting_rows = [row for row in data if str(get_value(row, "lock_status")).upper() == "WAITING"]
350 if any(norm(get_value(row, "lock_mode")) == "" for row in waiting_rows):
351 raise SystemExit("WAITING rows must include lock_mode")
352 if any(norm(get_value(row, "wait_resource")) == "" for row in waiting_rows):
353 raise SystemExit("WAITING rows must include wait_resource")
354
355 lock_mode_re = re.compile(r"^[A-Za-z0-9_-]+$")
356 if any(not lock_mode_re.match(norm(get_value(row, "lock_mode"))) for row in waiting_rows):
357 raise SystemExit("WAITING rows must include a valid lock_mode")
358
359 victim_counts = {}
360 expected_db = "netdata"
361 has_database = False
362 for row in data:
363 deadlock_id = norm(get_value(row, "deadlock_id"))
364 if deadlock_id == "":
365 raise SystemExit("deadlock_id missing from deadlock-info output")
366 process_id = norm(get_value(row, "process_id"))
367 if process_id == "":
368 raise SystemExit("process_id missing from deadlock-info output")
369 row_id = norm(get_value(row, "row_id"))
370 if row_id != f"{deadlock_id}:{process_id}":
371 raise SystemExit(f"row_id {row_id} does not match deadlock_id/process_id")
372 victim_counts.setdefault(deadlock_id, 0)
373 if str(get_value(row, "is_victim")).lower() == "true":
374 victim_counts[deadlock_id] += 1
375 db_val = norm(get_value(row, "database")).lower()
376 if db_val:
377 has_database = True
378 if db_val != expected_db:
379 raise SystemExit(f"unexpected database value {db_val!r}, expected {expected_db!r}")
380
381 for deadlock_id, count in victim_counts.items():
382 if count != 1:
383 raise SystemExit(f"deadlock_id {deadlock_id} has victim count {count}, expected 1")
384 if not has_database:
385 raise SystemExit("expected at least one row with database populated")
386 PY
387 return
388 fi
389
390 python - "$input" <<'PY'
391 import json
392 import re
393 import sys
394
395 path = sys.argv[1]
396 with open(path, "r") as fh:
397 doc = json.load(fh)
398
399 try:
400 status = int(doc.get("status"))
401 except (TypeError, ValueError):
402 raise SystemExit("unexpected status value: %r" % (doc.get("status"),))
403
404 if status != 200:
405 raise SystemExit("expected status 200, got %s" % status)
406
407 if doc.get("errorMessage"):
408 raise SystemExit("unexpected errorMessage on status 200: %r" % (doc.get("errorMessage"),))
409
410 columns = doc.get("columns") or {}
411 field_to_idx = {}
412 if isinstance(columns, dict):
413 for field, col in columns.items():
414 if not isinstance(col, dict):
415 continue
416 try:
417 field_to_idx[field] = int(col.get("index"))
418 except (TypeError, ValueError):
419 continue
420 else:
421 for idx, col in enumerate(columns):
422 if not isinstance(col, dict):
423 continue
424 field = col.get("field")
425 if field:
426 field_to_idx[field] = idx
427
428 for required in ("row_id", "deadlock_id", "process_id", "is_victim", "lock_mode", "lock_status", "query_text", "wait_resource", "database"):
429 if required not in field_to_idx:
430 raise SystemExit("missing expected column: %s" % required)
431
432 data = doc.get("data") or []
433 if not data:
434 raise SystemExit("deadlock-info returned no rows")
435
436 def get_value(row, field):
437 idx = field_to_idx[field]
438 return row[idx] if idx < len(row) else None
439
440 def norm(val):
441 return "" if val is None else str(val).strip()
442
443 has_waiting = any(str(get_value(row, "lock_status")).upper() == "WAITING" for row in data)
444 if not has_waiting:
445 raise SystemExit("no WAITING lock_status found in deadlock-info output")
446
447 table_pattern = re.compile(r"deadlock_(a|b)", re.IGNORECASE)
448 has_expected_query = any(table_pattern.search(str(get_value(row, "query_text"))) for row in data)
449 if not has_expected_query:
450 raise SystemExit("query_text does not reference deadlock tables")
451
452 waiting_rows = [row for row in data if str(get_value(row, "lock_status")).upper() == "WAITING"]
453 if any(norm(get_value(row, "lock_mode")) == "" for row in waiting_rows):
454 raise SystemExit("WAITING rows must include lock_mode")
455 if any(norm(get_value(row, "wait_resource")) == "" for row in waiting_rows):
456 raise SystemExit("WAITING rows must include wait_resource")
457
458 lock_mode_re = re.compile(r"^[A-Za-z0-9_-]+$")
459 if any(not lock_mode_re.match(norm(get_value(row, "lock_mode"))) for row in waiting_rows):
460 raise SystemExit("WAITING rows must include a valid lock_mode")
461
462 victim_counts = {}
463 expected_db = "netdata"
464 has_database = False
465 for row in data:
466 deadlock_id = norm(get_value(row, "deadlock_id"))
467 if deadlock_id == "":
468 raise SystemExit("deadlock_id missing from deadlock-info output")
469 process_id = norm(get_value(row, "process_id"))
470 if process_id == "":
471 raise SystemExit("process_id missing from deadlock-info output")
472 row_id = norm(get_value(row, "row_id"))
473 if row_id != "%s:%s" % (deadlock_id, process_id):
474 raise SystemExit("row_id %s does not match deadlock_id/process_id" % row_id)
475 victim_counts.setdefault(deadlock_id, 0)
476 if str(get_value(row, "is_victim")).lower() == "true":
477 victim_counts[deadlock_id] += 1
478 db_val = norm(get_value(row, "database")).lower()
479 if db_val:
480 has_database = True
481 if db_val != expected_db:
482 raise SystemExit("unexpected database value %r, expected %r" % (db_val, expected_db))
483
484 for deadlock_id, count in victim_counts.items():
485 if count != 1:
486 raise SystemExit("deadlock_id %s has victim count %s, expected 1" % (deadlock_id, count))
487 if not has_database:
488 raise SystemExit("expected at least one row with database populated")
489 PY
490 }
491
492 assert_deadlock_info_empty_success() {
493 local input="$1"
494
495 if command -v python3 >/dev/null 2>&1; then
496 python3 - "$input" <<'PY'
497 import json
498 import sys
499
500 path = sys.argv[1]
501 with open(path, "r", encoding="utf-8") as fh:
502 doc = json.load(fh)
503
504 try:
505 status = int(doc.get("status"))
506 except (TypeError, ValueError):
507 raise SystemExit(f"unexpected status value: {doc.get('status')!r}")
508
509 if status != 200:
510 raise SystemExit(f"expected status 200, got {status}")
511
512 if doc.get("errorMessage"):
513 raise SystemExit(f"unexpected errorMessage on status 200: {doc.get('errorMessage')!r}")
514
515 columns = doc.get("columns") or {}
516 field_to_idx = {}
517 if isinstance(columns, dict):
518 for field, col in columns.items():
519 if not isinstance(col, dict):
520 continue
521 try:
522 field_to_idx[field] = int(col.get("index"))
523 except (TypeError, ValueError):
524 continue
525 else:
526 for idx, col in enumerate(columns):
527 if not isinstance(col, dict):
528 continue
529 field = col.get("field")
530 if field:
531 field_to_idx[field] = idx
532
533 data = doc.get("data") or []
534 if len(data) == 0:
535 raise SystemExit(0)
536
537 query_idx = field_to_idx.get("query_text", None)
538 if query_idx is None:
539 raise SystemExit(f"expected no rows, got {len(data)}")
540
541 for row in data:
542 if query_idx >= len(row):
543 continue
544 query = str(row[query_idx]).lower()
545 if "deadlock_a" in query or "deadlock_b" in query:
546 raise SystemExit(f"unexpected deadlock rows for test tables, got {len(data)} rows")
547 PY
548 return
549 fi
550
551 python - "$input" <<'PY'
552 import json
553 import sys
554
555 path = sys.argv[1]
556 with open(path, "r") as fh:
557 doc = json.load(fh)
558
559 try:
560 status = int(doc.get("status"))
561 except (TypeError, ValueError):
562 raise SystemExit("unexpected status value: %r" % (doc.get("status"),))
563
564 if status != 200:
565 raise SystemExit("expected status 200, got %s" % status)
566
567 if doc.get("errorMessage"):
568 raise SystemExit("unexpected errorMessage on status 200: %r" % (doc.get("errorMessage"),))
569
570 columns = doc.get("columns") or {}
571 field_to_idx = {}
572 if isinstance(columns, dict):
573 for field, col in columns.items():
574 if not isinstance(col, dict):
575 continue
576 try:
577 field_to_idx[field] = int(col.get("index"))
578 except (TypeError, ValueError):
579 continue
580 else:
581 for idx, col in enumerate(columns):
582 if not isinstance(col, dict):
583 continue
584 field = col.get("field")
585 if field:
586 field_to_idx[field] = idx
587
588 data = doc.get("data") or []
589 if len(data) == 0:
590 raise SystemExit(0)
591
592 query_idx = field_to_idx.get("query_text", None)
593 if query_idx is None:
594 raise SystemExit("expected no rows, got %s" % len(data))
595
596 for row in data:
597 if query_idx >= len(row):
598 continue
599 query = str(row[query_idx]).lower()
600 if "deadlock_a" in query or "deadlock_b" in query:
601 raise SystemExit("unexpected deadlock rows for test tables, got %s rows" % len(data))
602 PY
603 }
604
605 assert_deadlock_info_error_contains() {
606 local input="$1"
607 local expected_status="$2"
608 local expected_substr="$3"
609
610 if command -v python3 >/dev/null 2>&1; then
611 python3 - "$input" "$expected_status" "$expected_substr" <<'PY'
612 import json
613 import sys
614
615 path = sys.argv[1]
616 expected_status = int(sys.argv[2])
617 expected = sys.argv[3].strip().lower()
618 with open(path, "r", encoding="utf-8") as fh:
619 doc = json.load(fh)
620
621 try:
622 status = int(doc.get("status"))
623 except (TypeError, ValueError):
624 raise SystemExit(f"unexpected status value: {doc.get('status')!r}")
625
626 if status != expected_status:
627 raise SystemExit(f"expected status {expected_status}, got {status}")
628
629 err = str(doc.get("errorMessage") or "").lower()
630 if expected not in err:
631 raise SystemExit(f"expected errorMessage to contain {expected!r}, got {err!r}")
632 PY
633 return
634 fi
635
636 python - "$input" "$expected_status" "$expected_substr" <<'PY'
637 import json
638 import sys
639
640 path = sys.argv[1]
641 expected_status = int(sys.argv[2])
642 expected = sys.argv[3].strip().lower()
643 with open(path, "r") as fh:
644 doc = json.load(fh)
645
646 try:
647 status = int(doc.get("status"))
648 except (TypeError, ValueError):
649 raise SystemExit("unexpected status value: %r" % (doc.get("status"),))
650
651 if status != expected_status:
652 raise SystemExit("expected status %s, got %s" % (expected_status, status))
653
654 err = str(doc.get("errorMessage") or "").lower()
655 if expected not in err:
656 raise SystemExit("expected errorMessage to contain %r, got %r" % (expected, err))
657 PY
658 }
659
660 assert_error_info_not_enabled() {
661 local input="$1"
662
663 if command -v python3 >/dev/null 2>&1; then
664 python3 - "$input" <<'PY'
665 import json
666 import sys
667
668 path = sys.argv[1]
669 with open(path, "r", encoding="utf-8") as fh:
670 doc = json.load(fh)
671
672 try:
673 status = int(doc.get("status"))
674 except (TypeError, ValueError):
675 raise SystemExit(f"unexpected status value: {doc.get('status')!r}")
676
677 if status < 400:
678 raise SystemExit(f"expected error status, got {status}")
679
680 err = str(doc.get("errorMessage") or "").lower()
681 if "not enabled" not in err:
682 raise SystemExit(f"expected errorMessage to contain 'not enabled', got {err!r}")
683 PY
684 return
685 fi
686
687 python - "$input" <<'PY'
688 import json
689 import sys
690
691 path = sys.argv[1]
692 with open(path, "r") as fh:
693 doc = json.load(fh)
694
695 try:
696 status = int(doc.get("status"))
697 except (TypeError, ValueError):
698 raise SystemExit("unexpected status value: %r" % (doc.get("status"),))
699
700 if status < 400:
701 raise SystemExit("expected error status, got %s" % status)
702
703 err = str(doc.get("errorMessage") or "").lower()
704 if "not enabled" not in err:
705 raise SystemExit("expected errorMessage to contain 'not enabled', got %r" % err)
706 PY
707 }
708
709 assert_error_info_has_errors() {
710 local input="$1"
711
712 if command -v python3 >/dev/null 2>&1; then
713 python3 - "$input" <<'PY'
714 import json
715 import sys
716
717 path = sys.argv[1]
718 with open(path, "r", encoding="utf-8") as fh:
719 doc = json.load(fh)
720
721 try:
722 status = int(doc.get("status"))
723 except (TypeError, ValueError):
724 raise SystemExit(f"unexpected status value: {doc.get('status')!r}")
725
726 if status != 200:
727 raise SystemExit(f"expected status 200, got {status}")
728
729 if doc.get("errorMessage"):
730 raise SystemExit(f"unexpected errorMessage on status 200: {doc.get('errorMessage')!r}")
731
732 columns = doc.get("columns") or {}
733 field_to_idx = {}
734 if isinstance(columns, dict):
735 for field, col in columns.items():
736 if not isinstance(col, dict):
737 continue
738 try:
739 field_to_idx[field] = int(col.get("index"))
740 except (TypeError, ValueError):
741 continue
742 else:
743 for idx, col in enumerate(columns):
744 if not isinstance(col, dict):
745 continue
746 field = col.get("field")
747 if field:
748 field_to_idx[field] = idx
749
750 for required in ("errorNumber", "errorMessage", "query"):
751 if required not in field_to_idx:
752 raise SystemExit(f"missing expected column: {required}")
753
754 data = doc.get("data") or []
755 if not data:
756 raise SystemExit("error-info returned no rows")
757
758 num_idx = field_to_idx["errorNumber"]
759 msg_idx = field_to_idx["errorMessage"]
760 query_idx = field_to_idx["query"]
761
762 # Error categories to verify:
763 # 208 - Invalid object name (table not found)
764 # 102 - Syntax error
765 # 2627 - Duplicate key / unique constraint violation
766 # 245 - Data type conversion error
767 # 8134 - Division by zero
768 error_categories = {
769 "table_not_found": {"patterns": ["invalid object name", "netdata_error_map_e2e"], "found": False},
770 "syntax_error": {"patterns": ["incorrect syntax", "form"], "found": False},
771 "duplicate_key": {"patterns": ["duplicate key", "unique", "primary key", "error_test"], "found": False},
772 "data_type": {"patterns": ["conversion failed", "converting"], "found": False},
773 "divide_by_zero": {"patterns": ["divide by zero"], "found": False},
774 }
775
776 for row in data:
777 if num_idx >= len(row) or row[num_idx] is None:
778 continue
779 msg = str(row[msg_idx]).lower() if msg_idx < len(row) else ""
780 query = str(row[query_idx]).lower() if query_idx < len(row) else ""
781 combined = msg + " " + query
782 for cat, info in error_categories.items():
783 if info["found"]:
784 continue
785 for pattern in info["patterns"]:
786 if pattern in combined:
787 info["found"] = True
788 break
789
790 missing = [cat for cat, info in error_categories.items() if not info["found"]]
791 if missing:
792 raise SystemExit(f"error-info missing error categories: {', '.join(missing)}")
793 PY
794 return
795 fi
796
797 python - "$input" <<'PY'
798 import json
799 import sys
800
801 path = sys.argv[1]
802 with open(path, "r") as fh:
803 doc = json.load(fh)
804
805 try:
806 status = int(doc.get("status"))
807 except (TypeError, ValueError):
808 raise SystemExit("unexpected status value: %r" % (doc.get("status"),))
809
810 if status != 200:
811 raise SystemExit("expected status 200, got %s" % status)
812
813 if doc.get("errorMessage"):
814 raise SystemExit("unexpected errorMessage on status 200: %r" % (doc.get("errorMessage"),))
815
816 columns = doc.get("columns") or {}
817 field_to_idx = {}
818 if isinstance(columns, dict):
819 for field, col in columns.items():
820 if not isinstance(col, dict):
821 continue
822 try:
823 field_to_idx[field] = int(col.get("index"))
824 except (TypeError, ValueError):
825 continue
826 else:
827 for idx, col in enumerate(columns):
828 if not isinstance(col, dict):
829 continue
830 field = col.get("field")
831 if field:
832 field_to_idx[field] = idx
833
834 for required in ("errorNumber", "errorMessage", "query"):
835 if required not in field_to_idx:
836 raise SystemExit("missing expected column: %s" % required)
837
838 data = doc.get("data") or []
839 if not data:
840 raise SystemExit("error-info returned no rows")
841
842 num_idx = field_to_idx["errorNumber"]
843 msg_idx = field_to_idx["errorMessage"]
844 query_idx = field_to_idx["query"]
845
846 # Error categories to verify:
847 # 208 - Invalid object name (table not found)
848 # 102 - Syntax error
849 # 2627 - Duplicate key / unique constraint violation
850 # 245 - Data type conversion error
851 # 8134 - Division by zero
852 error_categories = {
853 "table_not_found": {"patterns": ["invalid object name", "netdata_error_map_e2e"], "found": False},
854 "syntax_error": {"patterns": ["incorrect syntax", "form"], "found": False},
855 "duplicate_key": {"patterns": ["duplicate key", "unique", "primary key", "error_test"], "found": False},
856 "data_type": {"patterns": ["conversion failed", "converting"], "found": False},
857 "divide_by_zero": {"patterns": ["divide by zero"], "found": False},
858 }
859
860 for row in data:
861 if num_idx >= len(row) or row[num_idx] is None:
862 continue
863 msg = str(row[msg_idx]).lower() if msg_idx < len(row) else ""
864 query = str(row[query_idx]).lower() if query_idx < len(row) else ""
865 combined = msg + " " + query
866 for cat, info in error_categories.items():
867 if info["found"]:
868 continue
869 for pattern in info["patterns"]:
870 if pattern in combined:
871 info["found"] = True
872 break
873
874 missing = [cat for cat, info in error_categories.items() if not info["found"]]
875 if missing:
876 raise SystemExit("error-info missing error categories: %s" % ", ".join(missing))
877 PY
878 }
879
880 assert_top_queries_error_attribution_not_enabled() {
881 local input="$1"
882
883 if command -v python3 >/dev/null 2>&1; then
884 python3 - "$input" <<'PY'
885 import json
886 import sys
887
888 path = sys.argv[1]
889 with open(path, "r", encoding="utf-8") as fh:
890 doc = json.load(fh)
891
892 columns = doc.get("columns") or {}
893 field_to_idx = {}
894 if isinstance(columns, dict):
895 for field, col in columns.items():
896 if not isinstance(col, dict):
897 continue
898 try:
899 field_to_idx[field] = int(col.get("index"))
900 except (TypeError, ValueError):
901 continue
902 else:
903 for idx, col in enumerate(columns):
904 if not isinstance(col, dict):
905 continue
906 field = col.get("field")
907 if field:
908 field_to_idx[field] = idx
909
910 if "errorAttribution" not in field_to_idx:
911 raise SystemExit("missing expected column: errorAttribution")
912
913 data = doc.get("data") or []
914 idx = field_to_idx["errorAttribution"]
915 for row in data:
916 if idx >= len(row):
917 continue
918 if str(row[idx]) != "not_enabled":
919 raise SystemExit(f"expected errorAttribution 'not_enabled', got {row[idx]!r}")
920 PY
921 return
922 fi
923
924 python - "$input" <<'PY'
925 import json
926 import sys
927
928 path = sys.argv[1]
929 with open(path, "r") as fh:
930 doc = json.load(fh)
931
932 columns = doc.get("columns") or {}
933 field_to_idx = {}
934 if isinstance(columns, dict):
935 for field, col in columns.items():
936 if not isinstance(col, dict):
937 continue
938 try:
939 field_to_idx[field] = int(col.get("index"))
940 except (TypeError, ValueError):
941 continue
942 else:
943 for idx, col in enumerate(columns):
944 if not isinstance(col, dict):
945 continue
946 field = col.get("field")
947 if field:
948 field_to_idx[field] = idx
949
950 if "errorAttribution" not in field_to_idx:
951 raise SystemExit("missing expected column: errorAttribution")
952
953 data = doc.get("data") or []
954 idx = field_to_idx["errorAttribution"]
955 for row in data:
956 if idx >= len(row):
957 continue
958 if str(row[idx]) != "not_enabled":
959 raise SystemExit("expected errorAttribution 'not_enabled', got %r" % row[idx])
960 PY
961 }
962
963 assert_top_queries_error_attribution_active() {
964 local input="$1"
965
966 if command -v python3 >/dev/null 2>&1; then
967 python3 - "$input" <<'PY'
968 import json
969 import sys
970
971 path = sys.argv[1]
972 with open(path, "r", encoding="utf-8") as fh:
973 doc = json.load(fh)
974
975 columns = doc.get("columns") or {}
976 field_to_idx = {}
977 if isinstance(columns, dict):
978 for field, col in columns.items():
979 if not isinstance(col, dict):
980 continue
981 try:
982 field_to_idx[field] = int(col.get("index"))
983 except (TypeError, ValueError):
984 continue
985 else:
986 for idx, col in enumerate(columns):
987 if not isinstance(col, dict):
988 continue
989 field = col.get("field")
990 if field:
991 field_to_idx[field] = idx
992
993 for required in ("errorAttribution",):
994 if required not in field_to_idx:
995 raise SystemExit(f"missing expected column: {required}")
996
997 data = doc.get("data") or []
998 status_idx = field_to_idx["errorAttribution"]
999
1000 for row in data:
1001 if status_idx >= len(row):
1002 continue
1003 status = str(row[status_idx])
1004 if status not in ("enabled", "no_data"):
1005 raise SystemExit(f"unexpected errorAttribution status {status!r}")
1006 PY
1007 return
1008 fi
1009
1010 python - "$input" <<'PY'
1011 import json
1012 import sys
1013
1014 path = sys.argv[1]
1015 with open(path, "r") as fh:
1016 doc = json.load(fh)
1017
1018 columns = doc.get("columns") or {}
1019 field_to_idx = {}
1020 if isinstance(columns, dict):
1021 for field, col in columns.items():
1022 if not isinstance(col, dict):
1023 continue
1024 try:
1025 field_to_idx[field] = int(col.get("index"))
1026 except (TypeError, ValueError):
1027 continue
1028 else:
1029 for idx, col in enumerate(columns):
1030 if not isinstance(col, dict):
1031 continue
1032 field = col.get("field")
1033 if field:
1034 field_to_idx[field] = idx
1035
1036 for required in ("errorAttribution",):
1037 if required not in field_to_idx:
1038 raise SystemExit("missing expected column: %s" % required)
1039
1040 data = doc.get("data") or []
1041 status_idx = field_to_idx["errorAttribution"]
1042
1043 for row in data:
1044 if status_idx >= len(row):
1045 continue
1046 status = str(row[status_idx])
1047 if status not in ("enabled", "no_data"):
1048 raise SystemExit("unexpected errorAttribution status %r" % status)
1049 PY
1050 }
1051
1052 assert_top_queries_error_attribution_mapped() {
1053 local top_queries="$1"
1054 local error_info="$2"
1055
1056 if command -v python3 >/dev/null 2>&1; then
1057 python3 - "$top_queries" "$error_info" <<'PY'
1058 import json
1059 import sys
1060
1061 top_path = sys.argv[1]
1062 err_path = sys.argv[2]
1063
1064 with open(err_path, "r", encoding="utf-8") as fh:
1065 err_doc = json.load(fh)
1066
1067 err_cols = err_doc.get("columns") or {}
1068 err_idx = {}
1069 if isinstance(err_cols, dict):
1070 for field, col in err_cols.items():
1071 if not isinstance(col, dict):
1072 continue
1073 try:
1074 err_idx[field] = int(col.get("index"))
1075 except (TypeError, ValueError):
1076 continue
1077 else:
1078 for idx, col in enumerate(err_cols):
1079 if not isinstance(col, dict):
1080 continue
1081 field = col.get("field")
1082 if field:
1083 err_idx[field] = idx
1084
1085 for required in ("errorMessage", "errorNumber", "query", "queryHash"):
1086 if required not in err_idx:
1087 raise SystemExit(f"missing expected error-info column: {required}")
1088
1089 def normalize(text: str) -> str:
1090 return " ".join(text.split()).strip().rstrip(";").strip()
1091
1092 error_rows = err_doc.get("data") or []
1093 candidates = []
1094 for row in error_rows:
1095 msg = str(row[err_idx["errorMessage"]]).lower() if err_idx["errorMessage"] < len(row) else ""
1096 err_no = row[err_idx["errorNumber"]] if err_idx["errorNumber"] < len(row) else None
1097 query = str(row[err_idx["query"]]).lower() if err_idx["query"] < len(row) else ""
1098 qh = row[err_idx["queryHash"]] if err_idx["queryHash"] < len(row) else None
1099 try:
1100 err_no_val = int(err_no)
1101 except Exception:
1102 continue
1103 if err_no_val != 208:
1104 continue
1105 if "invalid object name" in msg and "netdata_error_map_e2e" in query:
1106 candidates.append((str(qh) if qh else "", normalize(query)))
1107
1108 if not candidates:
1109 raise SystemExit("no error-info row contained invalid object name for netdata_error_map_e2e")
1110
1111 with open(top_path, "r", encoding="utf-8") as fh:
1112 doc = json.load(fh)
1113
1114 columns = doc.get("columns") or {}
1115 field_to_idx = {}
1116 if isinstance(columns, dict):
1117 for field, col in columns.items():
1118 if not isinstance(col, dict):
1119 continue
1120 try:
1121 field_to_idx[field] = int(col.get("index"))
1122 except (TypeError, ValueError):
1123 continue
1124 else:
1125 for idx, col in enumerate(columns):
1126 if not isinstance(col, dict):
1127 continue
1128 field = col.get("field")
1129 if field:
1130 field_to_idx[field] = idx
1131
1132 for required in ("query", "queryHash", "errorAttribution", "errorNumber", "errorMessage"):
1133 if required not in field_to_idx:
1134 raise SystemExit(f"missing expected column: {required}")
1135
1136 data = doc.get("data") or []
1137 status_idx = field_to_idx["errorAttribution"]
1138 num_idx = field_to_idx["errorNumber"]
1139 msg_idx = field_to_idx["errorMessage"]
1140 hash_idx = field_to_idx["queryHash"]
1141
1142 matched = False
1143 for row in data:
1144 if status_idx >= len(row):
1145 continue
1146 if hash_idx >= len(row):
1147 continue
1148 status = str(row[status_idx]) if status_idx < len(row) else ""
1149 if status != "enabled":
1150 continue
1151 err_no = row[num_idx] if num_idx < len(row) else None
1152 try:
1153 err_no_val = int(err_no)
1154 except Exception:
1155 continue
1156 if err_no_val != 208:
1157 continue
1158 msg = str(row[msg_idx]).lower() if msg_idx < len(row) and row[msg_idx] is not None else ""
1159 if "invalid object name" not in msg:
1160 continue
1161 row_hash = str(row[hash_idx]) if hash_idx < len(row) and row[hash_idx] is not None else ""
1162 row_query = normalize(str(row[field_to_idx["query"]]).lower()) if field_to_idx["query"] < len(row) else ""
1163 for cand_hash, cand_query in candidates:
1164 if cand_hash and row_hash == cand_hash:
1165 matched = True
1166 break
1167 if cand_query and row_query == cand_query:
1168 matched = True
1169 break
1170 if matched:
1171 break
1172
1173 if not matched:
1174 raise SystemExit("no top-queries row had enabled error attribution for netdata_error_map_e2e")
1175 PY
1176 return
1177 fi
1178
1179 python - "$top_queries" "$error_info" <<'PY'
1180 import json
1181 import sys
1182
1183 top_path = sys.argv[1]
1184 err_path = sys.argv[2]
1185
1186 with open(err_path, "r") as fh:
1187 err_doc = json.load(fh)
1188
1189 err_cols = err_doc.get("columns") or {}
1190 err_idx = {}
1191 if isinstance(err_cols, dict):
1192 for field, col in err_cols.items():
1193 if not isinstance(col, dict):
1194 continue
1195 try:
1196 err_idx[field] = int(col.get("index"))
1197 except (TypeError, ValueError):
1198 continue
1199 else:
1200 for idx, col in enumerate(err_cols):
1201 if not isinstance(col, dict):
1202 continue
1203 field = col.get("field")
1204 if field:
1205 err_idx[field] = idx
1206
1207 for required in ("errorMessage", "errorNumber", "query", "queryHash"):
1208 if required not in err_idx:
1209 raise SystemExit("missing expected error-info column: %s" % required)
1210
1211 def normalize(text):
1212 return " ".join(text.split()).strip().rstrip(";").strip()
1213
1214 error_rows = err_doc.get("data") or []
1215 candidates = []
1216 for row in error_rows:
1217 msg = str(row[err_idx["errorMessage"]]).lower() if err_idx["errorMessage"] < len(row) else ""
1218 err_no = row[err_idx["errorNumber"]] if err_idx["errorNumber"] < len(row) else None
1219 query = str(row[err_idx["query"]]).lower() if err_idx["query"] < len(row) else ""
1220 qh = row[err_idx["queryHash"]] if err_idx["queryHash"] < len(row) else None
1221 try:
1222 err_no_val = int(err_no)
1223 except Exception:
1224 continue
1225 if err_no_val != 208:
1226 continue
1227 if "invalid object name" in msg and "netdata_error_map_e2e" in query:
1228 candidates.append((str(qh) if qh else "", normalize(query)))
1229
1230 if not candidates:
1231 raise SystemExit("no error-info row contained invalid object name for netdata_error_map_e2e")
1232
1233 with open(top_path, "r") as fh:
1234 doc = json.load(fh)
1235
1236 columns = doc.get("columns") or {}
1237 field_to_idx = {}
1238 if isinstance(columns, dict):
1239 for field, col in columns.items():
1240 if not isinstance(col, dict):
1241 continue
1242 try:
1243 field_to_idx[field] = int(col.get("index"))
1244 except (TypeError, ValueError):
1245 continue
1246 else:
1247 for idx, col in enumerate(columns):
1248 if not isinstance(col, dict):
1249 continue
1250 field = col.get("field")
1251 if field:
1252 field_to_idx[field] = idx
1253
1254 for required in ("query", "queryHash", "errorAttribution", "errorNumber", "errorMessage"):
1255 if required not in field_to_idx:
1256 raise SystemExit("missing expected column: %s" % required)
1257
1258 data = doc.get("data") or []
1259 status_idx = field_to_idx["errorAttribution"]
1260 num_idx = field_to_idx["errorNumber"]
1261 msg_idx = field_to_idx["errorMessage"]
1262 hash_idx = field_to_idx["queryHash"]
1263
1264 matched = False
1265 for row in data:
1266 if status_idx >= len(row):
1267 continue
1268 if hash_idx >= len(row):
1269 continue
1270 status = str(row[status_idx]) if status_idx < len(row) else ""
1271 if status != "enabled":
1272 continue
1273 err_no = row[num_idx] if num_idx < len(row) else None
1274 try:
1275 err_no_val = int(err_no)
1276 except Exception:
1277 continue
1278 if err_no_val != 208:
1279 continue
1280 msg = str(row[msg_idx]).lower() if msg_idx < len(row) and row[msg_idx] is not None else ""
1281 if "invalid object name" not in msg:
1282 continue
1283 row_hash = str(row[hash_idx]) if hash_idx < len(row) and row[hash_idx] is not None else ""
1284 row_query = normalize(str(row[field_to_idx["query"]]).lower()) if field_to_idx["query"] < len(row) else ""
1285 for cand_hash, cand_query in candidates:
1286 if cand_hash and row_hash == cand_hash:
1287 matched = True
1288 break
1289 if cand_query and row_query == cand_query:
1290 matched = True
1291 break
1292 if matched:
1293 break
1294
1295 if not matched:
1296 raise SystemExit("no top-queries row had enabled error attribution for netdata_error_map_e2e")
1297 PY
1298 }
1299
1300 assert_top_queries_plan_ops() {
1301 local input="$1"
1302
1303 if command -v python3 >/dev/null 2>&1; then
1304 python3 - "$input" <<'PY'
1305 import json
1306 import sys
1307
1308 path = sys.argv[1]
1309 with open(path, "r", encoding="utf-8") as fh:
1310 doc = json.load(fh)
1311
1312 columns = doc.get("columns") or {}
1313 field_to_idx = {}
1314 if isinstance(columns, dict):
1315 for field, col in columns.items():
1316 if not isinstance(col, dict):
1317 continue
1318 try:
1319 field_to_idx[field] = int(col.get("index"))
1320 except (TypeError, ValueError):
1321 continue
1322 else:
1323 for idx, col in enumerate(columns):
1324 if not isinstance(col, dict):
1325 continue
1326 field = col.get("field")
1327 if field:
1328 field_to_idx[field] = idx
1329
1330 for required in ("query", "hashMatch", "sorts"):
1331 if required not in field_to_idx:
1332 raise SystemExit(f"missing expected column: {required}")
1333
1334 data = doc.get("data") or []
1335 query_idx = field_to_idx["query"]
1336 hash_idx = field_to_idx["hashMatch"]
1337 sort_idx = field_to_idx["sorts"]
1338
1339 matched = False
1340 for row in data:
1341 if query_idx >= len(row):
1342 continue
1343 query = str(row[query_idx]).lower()
1344 if "join" not in query or "sample" not in query:
1345 continue
1346 hash_val = row[hash_idx] if hash_idx < len(row) else 0
1347 sort_val = row[sort_idx] if sort_idx < len(row) else 0
1348 try:
1349 hash_val = int(hash_val)
1350 except Exception:
1351 hash_val = 0
1352 try:
1353 sort_val = int(sort_val)
1354 except Exception:
1355 sort_val = 0
1356 if hash_val > 0 and sort_val > 0:
1357 matched = True
1358 break
1359
1360 if not matched:
1361 raise SystemExit("no top-queries row had hashMatch and sorts counts for the join query")
1362 PY
1363 return
1364 fi
1365
1366 python - "$input" <<'PY'
1367 import json
1368 import sys
1369
1370 path = sys.argv[1]
1371 with open(path, "r") as fh:
1372 doc = json.load(fh)
1373
1374 columns = doc.get("columns") or {}
1375 field_to_idx = {}
1376 if isinstance(columns, dict):
1377 for field, col in columns.items():
1378 if not isinstance(col, dict):
1379 continue
1380 try:
1381 field_to_idx[field] = int(col.get("index"))
1382 except (TypeError, ValueError):
1383 continue
1384 else:
1385 for idx, col in enumerate(columns):
1386 if not isinstance(col, dict):
1387 continue
1388 field = col.get("field")
1389 if field:
1390 field_to_idx[field] = idx
1391
1392 for required in ("query", "hashMatch", "sorts"):
1393 if required not in field_to_idx:
1394 raise SystemExit("missing expected column: %s" % required)
1395
1396 data = doc.get("data") or []
1397 query_idx = field_to_idx["query"]
1398 hash_idx = field_to_idx["hashMatch"]
1399 sort_idx = field_to_idx["sorts"]
1400
1401 matched = False
1402 for row in data:
1403 if query_idx >= len(row):
1404 continue
1405 query = str(row[query_idx]).lower()
1406 if "join" not in query or "sample" not in query:
1407 continue
1408 hash_val = row[hash_idx] if hash_idx < len(row) else 0
1409 sort_val = row[sort_idx] if sort_idx < len(row) else 0
1410 try:
1411 hash_val = int(hash_val)
1412 except Exception:
1413 hash_val = 0
1414 try:
1415 sort_val = int(sort_val)
1416 except Exception:
1417 sort_val = 0
1418 if hash_val > 0 and sort_val > 0:
1419 matched = True
1420 break
1421
1422 if not matched:
1423 raise SystemExit("no top-queries row had hashMatch and sorts counts for the join query")
1424 PY
1425 }
1426
1427 verify_deadlock_info_no_deadlock() {
1428 local output
1429
1430 output="$(run_mssql_function_with_retry deadlock-info '__job:local' 'false')"
1431 validate "$output"
1432 assert_deadlock_info_empty_success "$output"
1433 }
1434
1435 verify_deadlock_info() {
1436 local attempt
1437 local output
1438 local found="false"
1439
1440 for attempt in 1 2 3 4 5; do
1441 induce_deadlock_once
1442 output="$(run_mssql_function_with_retry deadlock-info '__job:local' 'false')"
1443 if has_min_rows "$output" 1; then
1444 validate "$output" --min-rows 1
1445 if assert_deadlock_info_content "$output"; then
1446 found="true"
1447 break
1448 fi
1449 fi
1450 sleep 1
1451 done
1452
1453 if [ "$found" != "true" ]; then
1454 echo "deadlock-info did not produce valid deadlock attribution after 5 attempts" >&2
1455 return 1
1456 fi
1457
1458 # Verify column visibility rules
1459 assert_column_visibility "$output" "deadlock-info"
1460 }
1461
1462 verify_deadlock_info_no_deadlock
1463 verify_deadlock_info
1464
1465 assert_top_queries_error_attribution_not_enabled "$WORKDIR/mssql-top-queries.json"
1466
1467 error_output="$(run_mssql_function_with_retry error-info '__job:local' 'false')"
1468 assert_error_info_not_enabled "$error_output"
1469
1470 mssql_exec_sa "IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = 'netdata_errors') DROP EVENT SESSION [netdata_errors] ON SERVER;"
1471 mssql_exec_sa "CREATE EVENT SESSION [netdata_errors] ON SERVER ADD EVENT sqlserver.error_reported(ACTION(sqlserver.sql_text, sqlserver.query_hash)) ADD TARGET package0.ring_buffer;"
1472 mssql_exec_sa "ALTER EVENT SESSION [netdata_errors] ON SERVER STATE = START;"
1473 mssql_exec_sa "ALTER DATABASE netdata SET QUERY_STORE (QUERY_CAPTURE_MODE = ALL, OPERATION_MODE = READ_WRITE);"
1474
1475 mssql_exec_sa "IF OBJECT_ID('dbo.netdata_error_map_e2e', 'U') IS NOT NULL DROP TABLE dbo.netdata_error_map_e2e;"
1476 mssql_exec_sa "CREATE TABLE dbo.netdata_error_map_e2e (id int NOT NULL PRIMARY KEY);"
1477 mssql_exec_sa "INSERT INTO dbo.netdata_error_map_e2e (id) VALUES (1), (2), (3);"
1478
1479 for _ in 1 2 3 4 5 6 7 8 9 10; do
1480 mssql_exec_sa "SELECT COUNT(*) FROM dbo.netdata_error_map_e2e;"
1481 done
1482
1483 mssql_exec_sa "DROP TABLE dbo.netdata_error_map_e2e;"
1484
1485 # Generate errors for multiple categories:
1486 # 1. Table not found (error 208)
1487 for _ in 1 2 3; do
1488 mssql_exec_sa_allow_error "SELECT COUNT(*) FROM dbo.netdata_error_map_e2e;"
1489 done
1490
1491 # 2. Syntax error (error 102)
1492 for _ in 1 2 3; do
1493 mssql_exec_sa_allow_error "SELECT * FORM dbo.sample;"
1494 done
1495
1496 # 3. Duplicate key / constraint violation (error 2627)
1497 for _ in 1 2 3; do
1498 mssql_exec_sa_allow_error "INSERT INTO dbo.error_test (id, unique_col, int_col) VALUES (1, 'new_value', 200);"
1499 mssql_exec_sa_allow_error "INSERT INTO dbo.error_test (id, unique_col, int_col) VALUES (99, 'existing_value', 300);"
1500 done
1501
1502 # 4. Data type conversion error (error 245)
1503 for _ in 1 2 3; do
1504 mssql_exec_sa_allow_error "SELECT CAST('not_a_number' AS INT);"
1505 done
1506
1507 # 5. Division by zero (error 8134)
1508 for _ in 1 2 3; do
1509 mssql_exec_sa_allow_error "SELECT 1/0;"
1510 done
1511
1512 for _ in 1 2 3 4 5; do
1513 mssql_exec_sa "SET NOCOUNT ON; SELECT a.id, b.name FROM dbo.sample a JOIN dbo.sample b ON a.id = b.id ORDER BY a.value + b.value DESC OPTION (HASH JOIN);"
1514 done
1515
1516 mssql_exec_sa "EXEC sys.sp_query_store_flush_db;"
1517 sleep 2
1518
1519 error_output="$(run_mssql_function_with_retry error-info '__job:local' 'true')"
1520 assert_error_info_has_errors "$error_output"
1521 assert_column_visibility "$error_output" "error-info"
1522 assert_unique_key_populated "$error_output" "error-info"
1523
1524 run_mssql_top_queries_with_retry
1525 assert_top_queries_error_attribution_active "$WORKDIR/mssql-top-queries.json"
1526 assert_top_queries_error_attribution_mapped "$WORKDIR/mssql-top-queries.json" "$WORKDIR/mssql-error-info.json"
1527 assert_top_queries_plan_ops "$WORKDIR/mssql-top-queries.json"
1528 assert_column_visibility "$WORKDIR/mssql-top-queries.json" "top-queries"
1529
1530 echo "E2E checks passed for ${MSSQL_VARIANT_LABEL}." >&2