opencode-skills-collection 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +6 -1
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/manage-skills/SKILL.md +187 -0
- package/bundled-skills/monte-carlo-monitor-creation/SKILL.md +222 -0
- package/bundled-skills/monte-carlo-monitor-creation/references/comparison-monitor.md +426 -0
- package/bundled-skills/monte-carlo-monitor-creation/references/custom-sql-monitor.md +207 -0
- package/bundled-skills/monte-carlo-monitor-creation/references/metric-monitor.md +292 -0
- package/bundled-skills/monte-carlo-monitor-creation/references/table-monitor.md +231 -0
- package/bundled-skills/monte-carlo-monitor-creation/references/validation-monitor.md +404 -0
- package/bundled-skills/monte-carlo-prevent/SKILL.md +252 -0
- package/bundled-skills/monte-carlo-prevent/references/TROUBLESHOOTING.md +23 -0
- package/bundled-skills/monte-carlo-prevent/references/parameters.md +32 -0
- package/bundled-skills/monte-carlo-prevent/references/workflows.md +478 -0
- package/bundled-skills/monte-carlo-push-ingestion/SKILL.md +363 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/anomaly-detection.md +87 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/custom-lineage.md +203 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/direct-http-api.md +207 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/prerequisites.md +150 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/push-lineage.md +160 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/push-metadata.md +158 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/push-query-logs.md +219 -0
- package/bundled-skills/monte-carlo-push-ingestion/references/validation.md +257 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/sample_verify.py +357 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_lineage.py +70 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_metadata.py +65 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_query_logs.py +70 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_lineage.py +214 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_metadata.py +160 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_query_logs.py +164 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_lineage.py +198 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_metadata.py +193 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_query_logs.py +207 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_and_push_metadata.py +71 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_and_push_query_logs.py +64 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_metadata.py +253 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_query_logs.py +149 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/push_metadata.py +190 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/push_query_logs.py +208 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_lineage.py +83 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_metadata.py +77 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_query_logs.py +83 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_lineage.py +240 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_metadata.py +212 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_query_logs.py +204 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_lineage.py +192 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_metadata.py +178 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_query_logs.py +200 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_lineage.py +119 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_metadata.py +119 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_query_logs.py +117 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_lineage.py +265 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_metadata.py +313 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_query_logs.py +284 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_lineage.py +309 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_metadata.py +245 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_query_logs.py +255 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_lineage.py +78 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_metadata.py +80 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_query_logs.py +88 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_lineage.py +235 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_metadata.py +219 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_query_logs.py +239 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_lineage.py +178 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_metadata.py +178 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_query_logs.py +196 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_lineage.py +154 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_metadata.py +137 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_query_logs.py +137 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_lineage.py +349 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_metadata.py +329 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_query_logs.py +254 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_lineage.py +307 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_metadata.py +228 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_query_logs.py +248 -0
- package/bundled-skills/monte-carlo-push-ingestion/scripts/test_template_sdk_usage.py +340 -0
- package/bundled-skills/monte-carlo-validation-notebook/SKILL.md +685 -0
- package/bundled-skills/monte-carlo-validation-notebook/scripts/generate_notebook_url.py +141 -0
- package/bundled-skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py +161 -0
- package/package.json +1 -1
- package/skills_index.json +503 -61
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Comparison Monitor Reference
|
|
2
|
+
|
|
3
|
+
Detailed reference for building `createComparisonMonitorMac` tool calls.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
Use a comparison monitor when the user wants to:
|
|
8
|
+
|
|
9
|
+
- Compare data between two tables (e.g., source vs target, dev vs prod)
|
|
10
|
+
- Validate data consistency after migration or replication
|
|
11
|
+
- Check row count parity across environments
|
|
12
|
+
- Compare field-level metrics between tables (null counts, sums, distributions)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Pre-Step: Verify Both Tables and Fields
|
|
17
|
+
|
|
18
|
+
Before constructing alert conditions, you MUST verify that both tables exist and that any referenced fields are real columns. This is the most common source of comparison monitor failures.
|
|
19
|
+
|
|
20
|
+
1. **Resolve both MCONs.** Use `search` to find the source and target tables. If the user provided `database:schema.table` format, search for each to get the MCON.
|
|
21
|
+
2. **Get full schemas.** Call `getTable` with `include_fields: true` on BOTH the source table and the target table. You need the column lists from both.
|
|
22
|
+
3. **For field-level metrics, verify fields exist on both sides.** Confirm that `sourceField` exists in the source table's column list AND `targetField` exists in the target table's column list. Field names are case-sensitive on most warehouses.
|
|
23
|
+
4. **Check field type compatibility.** The metric must be compatible with the column types on both sides. For example, `NUMERIC_MEAN` requires numeric columns in both the source and target tables. If the source column is numeric but the target is a string, the comparison will fail.
|
|
24
|
+
5. If any field does not exist or types are incompatible, stop and ask the user to clarify. Do not guess.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Required Parameters
|
|
29
|
+
|
|
30
|
+
| Parameter | Type | Description |
|
|
31
|
+
|-----------|------|-------------|
|
|
32
|
+
| `name` | string | Unique identifier for the monitor. Use a descriptive slug (e.g., `orders_dev_prod_compare`). |
|
|
33
|
+
| `description` | string | Human-readable description of what the monitor checks. |
|
|
34
|
+
| `source_table` | string | Source table MCON (preferred) or `database:schema.table` format. If not MCON, also pass `source_warehouse`. |
|
|
35
|
+
| `target_table` | string | Target table MCON (preferred) or `database:schema.table` format. If not MCON, also pass `target_warehouse`. |
|
|
36
|
+
| `alert_conditions` | array | List of comparison conditions (see Alert Conditions below). |
|
|
37
|
+
|
|
38
|
+
## Optional Parameters
|
|
39
|
+
|
|
40
|
+
| Parameter | Type | Description |
|
|
41
|
+
|-----------|------|-------------|
|
|
42
|
+
| `source_warehouse` | string | Warehouse name or UUID for the source table. Required if `source_table` is not an MCON. |
|
|
43
|
+
| `target_warehouse` | string | Warehouse name or UUID for the target table. Required if `target_table` is not an MCON. |
|
|
44
|
+
| `segment_fields` | array of string | Fields to segment the comparison by. Must exist in BOTH tables with the same name. |
|
|
45
|
+
| `domain_id` | string (uuid) | Domain UUID (use `getDomains` to list). Only one domain can be assigned per monitor. |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Cross-Warehouse Comparisons
|
|
50
|
+
|
|
51
|
+
When the source and target tables live in different warehouses (e.g., comparing a Snowflake staging table against a BigQuery production table), you MUST provide both `source_warehouse` and `target_warehouse` explicitly. The tool cannot auto-resolve warehouses when tables are in different environments.
|
|
52
|
+
|
|
53
|
+
Even when both tables are MCONs, if they belong to different warehouses, pass both warehouse parameters to be safe. Omitting them in cross-warehouse scenarios causes silent failures or incorrect results.
|
|
54
|
+
|
|
55
|
+
Common cross-warehouse patterns:
|
|
56
|
+
- **Dev vs prod:** same warehouse type, different databases or schemas
|
|
57
|
+
- **Migration validation:** source in old warehouse, target in new warehouse
|
|
58
|
+
- **Replication checks:** primary warehouse vs replica or downstream warehouse
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Alert Conditions
|
|
63
|
+
|
|
64
|
+
Each condition compares a metric between the source and target tables.
|
|
65
|
+
|
|
66
|
+
| Field | Type | Required | Description |
|
|
67
|
+
|-------|------|----------|-------------|
|
|
68
|
+
| `metric` | string | Yes | The metric to compare (see Metrics Reference below). |
|
|
69
|
+
| `sourceField` | string | For field-level metrics | Column in the source table. Required for ALL metrics except `ROW_COUNT`. |
|
|
70
|
+
| `targetField` | string | For field-level metrics | Column in the target table. Required for ALL metrics except `ROW_COUNT`. |
|
|
71
|
+
| `thresholdValue` | number | No | Threshold for acceptable difference between source and target. |
|
|
72
|
+
| `isThresholdRelative` | boolean | No | `false` = absolute difference (default), `true` = percentage difference. |
|
|
73
|
+
| `customMetric` | object | No | Custom SQL expressions for source and target (see Custom Metrics below). |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ROW_COUNT and Fields: A Critical Rule
|
|
78
|
+
|
|
79
|
+
> **NEVER pass `sourceField` or `targetField` when using the `ROW_COUNT` metric.**
|
|
80
|
+
|
|
81
|
+
`ROW_COUNT` is a table-level metric -- it counts all rows in the table, not values in a column. Passing field names with `ROW_COUNT` causes the API call to fail or produce unexpected behavior.
|
|
82
|
+
|
|
83
|
+
This is the single most common mistake with comparison monitors. Before submitting any alert condition with `ROW_COUNT`, verify that `sourceField` and `targetField` are both absent from the condition object.
|
|
84
|
+
|
|
85
|
+
| Metric | Fields needed? | What happens if you pass fields? |
|
|
86
|
+
|--------|---------------|----------------------------------|
|
|
87
|
+
| `ROW_COUNT` | **No -- NEVER pass fields** | API error or undefined behavior |
|
|
88
|
+
| All other metrics | **Yes -- always pass both fields** | Required for the comparison to work |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Metrics Reference
|
|
93
|
+
|
|
94
|
+
### Table-level metric (no fields needed)
|
|
95
|
+
|
|
96
|
+
| Metric | Description |
|
|
97
|
+
|--------|-------------|
|
|
98
|
+
| `ROW_COUNT` | Compare total row counts between source and target. |
|
|
99
|
+
|
|
100
|
+
### Field-level metrics (require `sourceField` and `targetField`)
|
|
101
|
+
|
|
102
|
+
#### Uniqueness and duplicates
|
|
103
|
+
|
|
104
|
+
| Metric | Description |
|
|
105
|
+
|--------|-------------|
|
|
106
|
+
| `UNIQUE_COUNT` | Count of distinct values. |
|
|
107
|
+
| `DUPLICATE_COUNT` | Count of duplicate (non-unique) values. |
|
|
108
|
+
| `APPROX_DISTINCT_COUNT` | Approximate distinct count (faster on large tables). |
|
|
109
|
+
|
|
110
|
+
#### Null and empty checks
|
|
111
|
+
|
|
112
|
+
| Metric | Description |
|
|
113
|
+
|--------|-------------|
|
|
114
|
+
| `NULL_COUNT` | Count of null values. |
|
|
115
|
+
| `NON_NULL_COUNT` | Count of non-null values. |
|
|
116
|
+
| `EMPTY_STRING_COUNT` | Count of empty string values. |
|
|
117
|
+
| `TEXT_ALL_SPACES_COUNT` | Count of values that are all whitespace. |
|
|
118
|
+
| `NAN_COUNT` | Count of NaN values. |
|
|
119
|
+
| `TEXT_NULL_KEYWORD_COUNT` | Count of values containing null-like keywords (e.g., "NULL", "None"). |
|
|
120
|
+
|
|
121
|
+
#### Numeric statistics
|
|
122
|
+
|
|
123
|
+
| Metric | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `NUMERIC_MEAN` | Mean of numeric field. |
|
|
126
|
+
| `NUMERIC_MEDIAN` | Median of numeric field. |
|
|
127
|
+
| `NUMERIC_MIN` | Minimum value. |
|
|
128
|
+
| `NUMERIC_MAX` | Maximum value. |
|
|
129
|
+
| `NUMERIC_STDDEV` | Standard deviation. |
|
|
130
|
+
| `SUM` | Sum of numeric field. |
|
|
131
|
+
| `ZERO_COUNT` | Count of zero values. |
|
|
132
|
+
| `NEGATIVE_COUNT` | Count of negative values. |
|
|
133
|
+
|
|
134
|
+
#### Percentiles
|
|
135
|
+
|
|
136
|
+
| Metric | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| `PERCENTILE_20` | 20th percentile value. |
|
|
139
|
+
| `PERCENTILE_40` | 40th percentile value. |
|
|
140
|
+
| `PERCENTILE_60` | 60th percentile value. |
|
|
141
|
+
| `PERCENTILE_80` | 80th percentile value. |
|
|
142
|
+
|
|
143
|
+
#### Text statistics
|
|
144
|
+
|
|
145
|
+
| Metric | Description |
|
|
146
|
+
|--------|-------------|
|
|
147
|
+
| `TEXT_MAX_LENGTH` | Maximum string length. |
|
|
148
|
+
| `TEXT_MIN_LENGTH` | Minimum string length. |
|
|
149
|
+
| `TEXT_MEAN_LENGTH` | Mean string length. |
|
|
150
|
+
| `TEXT_STD_LENGTH` | Standard deviation of string length. |
|
|
151
|
+
|
|
152
|
+
#### Text format checks
|
|
153
|
+
|
|
154
|
+
| Metric | Description |
|
|
155
|
+
|--------|-------------|
|
|
156
|
+
| `TEXT_NOT_INT_COUNT` | Count of values not parseable as integers. |
|
|
157
|
+
| `TEXT_NOT_NUMBER_COUNT` | Count of values not parseable as numbers. |
|
|
158
|
+
| `TEXT_NOT_UUID_COUNT` | Count of values not matching UUID format. |
|
|
159
|
+
| `TEXT_NOT_SSN_COUNT` | Count of values not matching SSN format. |
|
|
160
|
+
| `TEXT_NOT_US_PHONE_COUNT` | Count of values not matching US phone format. |
|
|
161
|
+
| `TEXT_NOT_US_STATE_CODE_COUNT` | Count of values not matching US state codes. |
|
|
162
|
+
| `TEXT_NOT_US_ZIP_CODE_COUNT` | Count of values not matching US zip codes. |
|
|
163
|
+
| `TEXT_NOT_EMAIL_ADDRESS_COUNT` | Count of values not matching email format. |
|
|
164
|
+
| `TEXT_NOT_TIMESTAMP_COUNT` | Count of values not parseable as timestamps. |
|
|
165
|
+
|
|
166
|
+
#### Boolean
|
|
167
|
+
|
|
168
|
+
| Metric | Description |
|
|
169
|
+
|--------|-------------|
|
|
170
|
+
| `TRUE_COUNT` | Count of true values. |
|
|
171
|
+
| `FALSE_COUNT` | Count of false values. |
|
|
172
|
+
|
|
173
|
+
#### Timestamp
|
|
174
|
+
|
|
175
|
+
| Metric | Description |
|
|
176
|
+
|--------|-------------|
|
|
177
|
+
| `FUTURE_TIMESTAMP_COUNT` | Count of timestamps in the future. |
|
|
178
|
+
| `PAST_TIMESTAMP_COUNT` | Count of timestamps unreasonably far in the past. |
|
|
179
|
+
| `UNIX_ZERO_COUNT` | Count of timestamps equal to Unix epoch zero (1970-01-01). |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Choosing the Right Metric
|
|
184
|
+
|
|
185
|
+
| User intent | Correct metric | Fields needed? |
|
|
186
|
+
|-------------|---------------|----------------|
|
|
187
|
+
| Row count parity | `ROW_COUNT` | **No** -- never pass fields |
|
|
188
|
+
| Distinct values in a column | `UNIQUE_COUNT` | Yes |
|
|
189
|
+
| Null values in a column | `NULL_COUNT` | Yes |
|
|
190
|
+
| Sum, average, min, max | `SUM`, `NUMERIC_MEAN`, `NUMERIC_MIN`, `NUMERIC_MAX` | Yes |
|
|
191
|
+
| Data completeness | `NON_NULL_COUNT` | Yes |
|
|
192
|
+
| String format validation | `TEXT_NOT_EMAIL_ADDRESS_COUNT`, `TEXT_NOT_UUID_COUNT`, etc. | Yes |
|
|
193
|
+
| Custom computed expressions | Use `customMetric` instead of `metric` | No (SQL handles it) |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Custom Metrics
|
|
198
|
+
|
|
199
|
+
Use custom metrics when:
|
|
200
|
+
|
|
201
|
+
- **Column names differ** between source and target and you need a computed expression (not just a direct field comparison).
|
|
202
|
+
- **You need a derived calculation** like `SUM(quantity * unit_price)` rather than a simple column metric.
|
|
203
|
+
- **Standard metrics do not cover the comparison** (e.g., comparing a ratio, a conditional aggregate, or a windowed calculation).
|
|
204
|
+
|
|
205
|
+
If the columns simply have different names but you want a standard metric (e.g., compare `SUM` of `revenue` in source vs `total_revenue` in target), you do NOT need a custom metric -- just use the standard metric with different `sourceField` and `targetField` values.
|
|
206
|
+
|
|
207
|
+
Custom metric structure:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"customMetric": {
|
|
212
|
+
"displayName": "Revenue Sum",
|
|
213
|
+
"sourceSqlExpression": "SUM(revenue)",
|
|
214
|
+
"targetSqlExpression": "SUM(total_revenue)"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Field | Type | Required | Description |
|
|
220
|
+
|-------|------|----------|-------------|
|
|
221
|
+
| `displayName` | string | Yes | Human-readable name for the metric in alerts and dashboards. |
|
|
222
|
+
| `sourceSqlExpression` | string | Yes | SQL expression evaluated against the source table. |
|
|
223
|
+
| `targetSqlExpression` | string | Yes | SQL expression evaluated against the target table. |
|
|
224
|
+
|
|
225
|
+
When using `customMetric`, do NOT also pass `metric`, `sourceField`, or `targetField` in the same alert condition. The custom metric replaces all of those.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Threshold Guidance
|
|
230
|
+
|
|
231
|
+
### Absolute thresholds (`isThresholdRelative: false` or omitted)
|
|
232
|
+
|
|
233
|
+
The `thresholdValue` is the maximum acceptable absolute difference between the source and target metric values.
|
|
234
|
+
|
|
235
|
+
- `thresholdValue: 0` -- source and target must match exactly.
|
|
236
|
+
- `thresholdValue: 100` -- up to 100 units of difference is acceptable.
|
|
237
|
+
|
|
238
|
+
### Relative (percentage) thresholds (`isThresholdRelative: true`)
|
|
239
|
+
|
|
240
|
+
The `thresholdValue` is the maximum acceptable percentage difference.
|
|
241
|
+
|
|
242
|
+
- `thresholdValue: 5` -- up to 5% difference is acceptable.
|
|
243
|
+
- `thresholdValue: 0.1` -- up to 0.1% difference is acceptable.
|
|
244
|
+
|
|
245
|
+
### When to use each
|
|
246
|
+
|
|
247
|
+
| Scenario | Recommended threshold type |
|
|
248
|
+
|----------|---------------------------|
|
|
249
|
+
| Exact replication (row counts must match) | Absolute, `thresholdValue: 0` |
|
|
250
|
+
| Near-real-time sync with small lag | Absolute, small value (e.g., 10-100) |
|
|
251
|
+
| Tables at different scales | Relative, percentage-based |
|
|
252
|
+
| Aggregated metrics (sums, means) | Relative, to handle floating-point differences |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Examples
|
|
257
|
+
|
|
258
|
+
### Row count parity with absolute threshold
|
|
259
|
+
|
|
260
|
+
Compare row counts between dev and prod, alerting if they differ by more than 100 rows.
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"name": "orders_dev_prod_row_count",
|
|
265
|
+
"description": "Verify dev and prod orders tables have similar row counts",
|
|
266
|
+
"source_table": "MCON++a1b2c3d4-e5f6-7890-abcd-ef1234567890++1++1++dev_warehouse:core.orders",
|
|
267
|
+
"target_table": "MCON++b2c3d4e5-f6a7-8901-bcde-f12345678901++1++1++prod_warehouse:core.orders",
|
|
268
|
+
"alert_conditions": [
|
|
269
|
+
{
|
|
270
|
+
"metric": "ROW_COUNT",
|
|
271
|
+
"thresholdValue": 100,
|
|
272
|
+
"isThresholdRelative": false
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Note: no `sourceField` or `targetField` -- `ROW_COUNT` is table-level.
|
|
279
|
+
|
|
280
|
+
### Row count parity with percentage threshold
|
|
281
|
+
|
|
282
|
+
Alert if row counts differ by more than 5%.
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"name": "orders_replication_check",
|
|
287
|
+
"description": "Verify replicated orders table is within 5% of source row count",
|
|
288
|
+
"source_table": "MCON++a1b2c3d4-e5f6-7890-abcd-ef1234567890++1++1++primary:sales.orders",
|
|
289
|
+
"target_table": "MCON++b2c3d4e5-f6a7-8901-bcde-f12345678901++1++1++replica:sales.orders",
|
|
290
|
+
"alert_conditions": [
|
|
291
|
+
{
|
|
292
|
+
"metric": "ROW_COUNT",
|
|
293
|
+
"thresholdValue": 5,
|
|
294
|
+
"isThresholdRelative": true
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Field-level comparison (different column names)
|
|
301
|
+
|
|
302
|
+
Compare the sum of `revenue` in the source table against `total_revenue` in the target table.
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{
|
|
306
|
+
"name": "revenue_source_target_sum",
|
|
307
|
+
"description": "Verify revenue sums match between staging and production",
|
|
308
|
+
"source_table": "MCON++a1b2c3d4-e5f6-7890-abcd-ef1234567890++1++1++staging:finance.transactions",
|
|
309
|
+
"target_table": "MCON++b2c3d4e5-f6a7-8901-bcde-f12345678901++1++1++production:finance.transactions",
|
|
310
|
+
"alert_conditions": [
|
|
311
|
+
{
|
|
312
|
+
"metric": "SUM",
|
|
313
|
+
"sourceField": "revenue",
|
|
314
|
+
"targetField": "total_revenue",
|
|
315
|
+
"thresholdValue": 1,
|
|
316
|
+
"isThresholdRelative": true
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Segmented comparison
|
|
323
|
+
|
|
324
|
+
Compare null counts on `email` between source and target, segmented by `country`. The `country` field must exist in both tables.
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"name": "email_nulls_by_country",
|
|
329
|
+
"description": "Compare email null counts by country between ETL source and target",
|
|
330
|
+
"source_table": "MCON++a1b2c3d4-e5f6-7890-abcd-ef1234567890++1++1++raw:crm.contacts",
|
|
331
|
+
"target_table": "MCON++b2c3d4e5-f6a7-8901-bcde-f12345678901++1++1++analytics:crm.contacts",
|
|
332
|
+
"segment_fields": ["country"],
|
|
333
|
+
"alert_conditions": [
|
|
334
|
+
{
|
|
335
|
+
"metric": "NULL_COUNT",
|
|
336
|
+
"sourceField": "email",
|
|
337
|
+
"targetField": "email",
|
|
338
|
+
"thresholdValue": 0,
|
|
339
|
+
"isThresholdRelative": false
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Cross-warehouse comparison with explicit warehouses
|
|
346
|
+
|
|
347
|
+
When source and target are in different warehouses, both warehouse parameters must be provided.
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"name": "migration_users_row_count",
|
|
352
|
+
"description": "Validate user row counts match after Snowflake to BigQuery migration",
|
|
353
|
+
"source_table": "snowflake_db:public.users",
|
|
354
|
+
"source_warehouse": "snowflake-prod",
|
|
355
|
+
"target_table": "bigquery_project:public.users",
|
|
356
|
+
"target_warehouse": "bigquery-prod",
|
|
357
|
+
"alert_conditions": [
|
|
358
|
+
{
|
|
359
|
+
"metric": "ROW_COUNT",
|
|
360
|
+
"thresholdValue": 0,
|
|
361
|
+
"isThresholdRelative": false
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Custom metric comparison
|
|
368
|
+
|
|
369
|
+
Compare a computed revenue expression when the SQL differs between source and target.
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"name": "computed_revenue_compare",
|
|
374
|
+
"description": "Compare total revenue computation between legacy and new schema",
|
|
375
|
+
"source_table": "MCON++a1b2c3d4-e5f6-7890-abcd-ef1234567890++1++1++warehouse:legacy.orders",
|
|
376
|
+
"target_table": "MCON++b2c3d4e5-f6a7-8901-bcde-f12345678901++1++1++warehouse:v2.orders",
|
|
377
|
+
"alert_conditions": [
|
|
378
|
+
{
|
|
379
|
+
"customMetric": {
|
|
380
|
+
"displayName": "Total Revenue",
|
|
381
|
+
"sourceSqlExpression": "SUM(quantity * unit_price)",
|
|
382
|
+
"targetSqlExpression": "SUM(total_amount)"
|
|
383
|
+
},
|
|
384
|
+
"thresholdValue": 0.01,
|
|
385
|
+
"isThresholdRelative": true
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Multiple alert conditions
|
|
392
|
+
|
|
393
|
+
Compare both row counts and field-level metrics in a single monitor.
|
|
394
|
+
|
|
395
|
+
```json
|
|
396
|
+
{
|
|
397
|
+
"name": "orders_full_comparison",
|
|
398
|
+
"description": "Full comparison of orders between staging and production",
|
|
399
|
+
"source_table": "MCON++a1b2c3d4-e5f6-7890-abcd-ef1234567890++1++1++staging:core.orders",
|
|
400
|
+
"target_table": "MCON++b2c3d4e5-f6a7-8901-bcde-f12345678901++1++1++production:core.orders",
|
|
401
|
+
"domain_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
402
|
+
"alert_conditions": [
|
|
403
|
+
{
|
|
404
|
+
"metric": "ROW_COUNT",
|
|
405
|
+
"thresholdValue": 0,
|
|
406
|
+
"isThresholdRelative": false
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"metric": "NULL_COUNT",
|
|
410
|
+
"sourceField": "customer_id",
|
|
411
|
+
"targetField": "customer_id",
|
|
412
|
+
"thresholdValue": 0,
|
|
413
|
+
"isThresholdRelative": false
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
"metric": "SUM",
|
|
417
|
+
"sourceField": "amount",
|
|
418
|
+
"targetField": "amount",
|
|
419
|
+
"thresholdValue": 0.1,
|
|
420
|
+
"isThresholdRelative": true
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Note: the `ROW_COUNT` condition has no fields, while the field-level conditions each specify both `sourceField` and `targetField`.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Custom SQL Monitor Reference
|
|
2
|
+
|
|
3
|
+
Detailed reference for building `createCustomSqlMonitorMac` tool calls.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
Use a custom SQL monitor when the user wants to:
|
|
8
|
+
|
|
9
|
+
- Run a specific SQL query and alert on its result
|
|
10
|
+
- Implement cross-table logic (joins, subqueries, CTEs)
|
|
11
|
+
- Apply business-specific aggregations or calculations that don't map to a single metric
|
|
12
|
+
- Monitor a condition that spans multiple columns or tables
|
|
13
|
+
- Use a SQL query they already have in mind
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The Universal Fallback
|
|
18
|
+
|
|
19
|
+
Custom SQL is the fallback monitor type. Reach for it whenever another monitor type cannot express what the user needs:
|
|
20
|
+
|
|
21
|
+
- **Validation monitor won't work** because the column doesn't exist yet, or the logic requires joins across tables.
|
|
22
|
+
- **Metric monitor can't express the business logic** -- for example, a ratio between two columns, a conditional aggregation, or a calculation that spans multiple tables.
|
|
23
|
+
- **Cross-table joins are needed** -- metric and validation monitors operate on a single table. If the check requires data from two or more tables, custom SQL is the only option.
|
|
24
|
+
- **The user already has a SQL query** -- don't force it into another monitor type. Wrap it in a custom SQL monitor.
|
|
25
|
+
|
|
26
|
+
If you find yourself contorting another monitor type to fit the user's intent, stop and use custom SQL instead.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Required Parameters
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Description |
|
|
33
|
+
|-----------|------|-------------|
|
|
34
|
+
| `name` | string | Unique identifier for the monitor. Use a descriptive slug (e.g., `orphan_orders_check`). |
|
|
35
|
+
| `description` | string | Human-readable description of what the monitor checks. |
|
|
36
|
+
| `warehouse` | string | Warehouse name or UUID where the SQL query will be executed. |
|
|
37
|
+
| `sql` | string | SQL query that returns a **single numeric value** (one row, one column). |
|
|
38
|
+
| `alert_conditions` | array | List of threshold conditions (see Alert Conditions below). |
|
|
39
|
+
|
|
40
|
+
## Optional Parameters
|
|
41
|
+
|
|
42
|
+
| Parameter | Type | Description |
|
|
43
|
+
|-----------|------|-------------|
|
|
44
|
+
| `domain_id` | string (uuid) | Domain UUID (use `getDomains` to list). Only one domain can be assigned per monitor. |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Alert Conditions
|
|
49
|
+
|
|
50
|
+
Each alert condition compares the query result against a threshold.
|
|
51
|
+
|
|
52
|
+
| Field | Type | Required | Description |
|
|
53
|
+
|-------|------|----------|-------------|
|
|
54
|
+
| `operator` | string | Yes | `"GT"`, `"LT"`, `"EQ"`, `"GTE"`, `"LTE"`, `"NE"` |
|
|
55
|
+
| `thresholdValue` | number | Yes | Numeric threshold to compare the query result against. |
|
|
56
|
+
|
|
57
|
+
### No AUTO Support
|
|
58
|
+
|
|
59
|
+
Custom SQL monitors do **NOT** support `AUTO` (anomaly detection). You must specify an explicit operator and threshold for every alert condition. This is a common mistake -- if the user asks for anomaly detection, steer them toward a metric monitor instead, which does support `AUTO`.
|
|
60
|
+
|
|
61
|
+
If the user is unsure what threshold to set, help them reason about it: "What value would indicate a problem? If the query returns X, should that fire an alert?"
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## SQL Query Requirements
|
|
66
|
+
|
|
67
|
+
The SQL query MUST return exactly **one row with one numeric column**. This is non-negotiable -- the monitor compares that single value against the alert conditions.
|
|
68
|
+
|
|
69
|
+
### Rules
|
|
70
|
+
|
|
71
|
+
- Use aggregate functions: `COUNT(*)`, `SUM()`, `AVG()`, `MAX()`, `MIN()`, or similar.
|
|
72
|
+
- Can reference any table, view, or materialized view accessible in the warehouse.
|
|
73
|
+
- Can use joins, subqueries, CTEs, window functions -- any valid SQL.
|
|
74
|
+
- Do **NOT** include trailing semicolons.
|
|
75
|
+
- Do **NOT** include comments (`--` or `/* */`) -- some warehouses strip them inconsistently.
|
|
76
|
+
|
|
77
|
+
### SQL Validation Tips
|
|
78
|
+
|
|
79
|
+
These are the most common mistakes that cause custom SQL monitors to fail or produce misleading results:
|
|
80
|
+
|
|
81
|
+
1. **Handle NULLs with COALESCE.** If your aggregate could return NULL (e.g., `SUM(amount)` on an empty result set), wrap it: `SELECT COALESCE(SUM(amount), 0) FROM ...`. A NULL result cannot be compared against a threshold and will not trigger alerts.
|
|
82
|
+
|
|
83
|
+
2. **Ensure exactly one row, one column.** If your query could return zero rows (e.g., a filtered `SELECT` with no `GROUP BY`), wrap it in an outer aggregate: `SELECT COUNT(*) FROM (SELECT ...) sub`. If it returns multiple columns, select only the one you need.
|
|
84
|
+
|
|
85
|
+
3. **Test the query mentally.** Before finalizing, ask: "If this query returns 5, will the alert condition fire correctly?" Walk through the logic with a concrete number.
|
|
86
|
+
|
|
87
|
+
4. **For time-windowed checks, use appropriate date functions.** SQL syntax for date arithmetic varies by warehouse (see Warehouse-Specific SQL Notes below). Always scope time windows to avoid scanning the entire table history.
|
|
88
|
+
|
|
89
|
+
5. **Avoid non-deterministic results.** Queries using `LIMIT` without `ORDER BY`, or `RANDOM()`, produce unpredictable results that make alerting unreliable.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Warehouse-Specific SQL Notes
|
|
94
|
+
|
|
95
|
+
SQL syntax for date arithmetic and functions varies across warehouses. When writing time-windowed queries, use the correct syntax for the user's warehouse:
|
|
96
|
+
|
|
97
|
+
| Operation | Snowflake | BigQuery | Redshift |
|
|
98
|
+
|-----------|-----------|----------|----------|
|
|
99
|
+
| Subtract 1 day from now | `DATEADD(day, -1, CURRENT_TIMESTAMP())` | `DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)` | `DATEADD(day, -1, GETDATE())` |
|
|
100
|
+
| Subtract 1 hour from now | `DATEADD(hour, -1, CURRENT_TIMESTAMP())` | `TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)` | `DATEADD(hour, -1, GETDATE())` |
|
|
101
|
+
| Current timestamp | `CURRENT_TIMESTAMP()` | `CURRENT_TIMESTAMP()` | `GETDATE()` |
|
|
102
|
+
| Date truncation | `DATE_TRUNC('day', col)` | `DATE_TRUNC(col, DAY)` | `DATE_TRUNC('day', col)` |
|
|
103
|
+
|
|
104
|
+
When unsure which warehouse the user is on, ask. Getting the syntax wrong causes the monitor to fail on every scheduled run.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Examples
|
|
109
|
+
|
|
110
|
+
### Orphan records (GT 0)
|
|
111
|
+
|
|
112
|
+
Alert when orders reference customers that don't exist.
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"name": "orphan_orders_check",
|
|
117
|
+
"description": "Detect orders referencing non-existent customers",
|
|
118
|
+
"warehouse": "production_snowflake",
|
|
119
|
+
"sql": "SELECT COUNT(*) FROM analytics.core.orders o LEFT JOIN analytics.core.customers c ON o.customer_id = c.id WHERE c.id IS NULL",
|
|
120
|
+
"alert_conditions": [
|
|
121
|
+
{
|
|
122
|
+
"operator": "GT",
|
|
123
|
+
"thresholdValue": 0
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Daily revenue floor (LT threshold)
|
|
130
|
+
|
|
131
|
+
Alert when total revenue for the past 24 hours drops below a minimum.
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"name": "daily_revenue_floor",
|
|
136
|
+
"description": "Alert when daily revenue falls below $10,000",
|
|
137
|
+
"warehouse": "production_snowflake",
|
|
138
|
+
"sql": "SELECT COALESCE(SUM(amount), 0) FROM analytics.billing.transactions WHERE created_at >= DATEADD(day, -1, CURRENT_TIMESTAMP())",
|
|
139
|
+
"alert_conditions": [
|
|
140
|
+
{
|
|
141
|
+
"operator": "LT",
|
|
142
|
+
"thresholdValue": 10000
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Duplicate rate exceeds threshold
|
|
149
|
+
|
|
150
|
+
Alert when the duplicate rate on a key field exceeds 1%.
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"name": "order_id_duplicate_rate",
|
|
155
|
+
"description": "Alert when order_id duplicate rate exceeds 1%",
|
|
156
|
+
"warehouse": "production_snowflake",
|
|
157
|
+
"sql": "SELECT COALESCE(1.0 - (COUNT(DISTINCT order_id) * 1.0 / NULLIF(COUNT(*), 0)), 0) FROM analytics.core.orders WHERE created_at >= DATEADD(day, -1, CURRENT_TIMESTAMP())",
|
|
158
|
+
"alert_conditions": [
|
|
159
|
+
{
|
|
160
|
+
"operator": "GT",
|
|
161
|
+
"thresholdValue": 0.01
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Multiple threshold conditions (range check)
|
|
168
|
+
|
|
169
|
+
Alert when a value falls outside an acceptable range. Multiple conditions act as independent checks -- each one that evaluates to true fires its own alert.
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"name": "avg_order_amount_range",
|
|
174
|
+
"description": "Alert when average order amount is outside the $20-$500 range",
|
|
175
|
+
"warehouse": "production_snowflake",
|
|
176
|
+
"sql": "SELECT COALESCE(AVG(amount), 0) FROM analytics.core.orders WHERE created_at >= DATEADD(day, -1, CURRENT_TIMESTAMP()) AND status = 'completed'",
|
|
177
|
+
"alert_conditions": [
|
|
178
|
+
{
|
|
179
|
+
"operator": "LT",
|
|
180
|
+
"thresholdValue": 20
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"operator": "GT",
|
|
184
|
+
"thresholdValue": 500
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Cross-table freshness check (BigQuery syntax)
|
|
191
|
+
|
|
192
|
+
Alert when the latest row in a downstream table is more than 2 hours behind the source.
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"name": "pipeline_lag_check",
|
|
197
|
+
"description": "Alert when downstream table lags source by more than 2 hours",
|
|
198
|
+
"warehouse": "production_bigquery",
|
|
199
|
+
"sql": "SELECT COALESCE(TIMESTAMP_DIFF(s.max_ts, t.max_ts, MINUTE), 9999) FROM (SELECT MAX(event_timestamp) AS max_ts FROM project.raw.events) s CROSS JOIN (SELECT MAX(processed_at) AS max_ts FROM project.analytics.events_processed) t",
|
|
200
|
+
"alert_conditions": [
|
|
201
|
+
{
|
|
202
|
+
"operator": "GT",
|
|
203
|
+
"thresholdValue": 120
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
```
|