bqlens 0.1.0__tar.gz

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.
bqlens-0.1.0/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Apache License 2.0
2
+
3
+ Copyright 2026 bqlens contributors
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
bqlens-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: bqlens
3
+ Version: 0.1.0
4
+ Summary: Find wasted BigQuery spend in one command. Read-only.
5
+ License: Apache-2.0
6
+ Keywords: bigquery,gcp,finops,cost-optimization,data-engineering
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Database
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: bigquery
15
+ Requires-Dist: google-cloud-bigquery>=3.11; extra == "bigquery"
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.4; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # bqlens
21
+
22
+ **Find wasted BigQuery spend in one command.**
23
+
24
+ No signup. No sales call. No agent installed in your VPC. It reads your query
25
+ history, tells you where the money is going, and tells you how to stop it.
26
+
27
+ ```bash
28
+ pip install bqlens
29
+ bqlens scan --project my-project --days 30
30
+ ```
31
+
32
+ ```
33
+ bqlens — my-project
34
+ 1,833 query jobs over the last 30 days
35
+
36
+ Spend in window $ 1,946.10
37
+ Projected monthly $ 1,946.10
38
+ Recoverable $ 572.50/mo (29% of spend)
39
+
40
+ 1. [HIGH] Wildcard table scans without _TABLE_SUFFIX (R004)
41
+ $179.30/mo recoverable · 22 jobs
42
+ ...
43
+ ```
44
+
45
+ Try it with no credentials at all:
46
+
47
+ ```bash
48
+ bqlens scan --demo
49
+ ```
50
+
51
+ ---
52
+
53
+ ## What it looks at
54
+
55
+ bqlens reads `INFORMATION_SCHEMA.JOBS_BY_PROJECT` — job metadata and the SQL
56
+ text of your queries.
57
+
58
+ **It never reads a row of your table data.** There is no row access, no
59
+ sampling, no data leaving your project. The whole tool is a read of your own
60
+ query log plus regex and arithmetic, running on your machine.
61
+
62
+ ## What it finds
63
+
64
+ | Rule | What it catches | Why it costs money |
65
+ |------|-----------------|--------------------|
66
+ | R001 | `SELECT *` on large scans | BigQuery bills per column read |
67
+ | R002 | `LIMIT` with no `WHERE` | LIMIT caps rows returned, not bytes scanned |
68
+ | R003 | Large scans with no filter | No partition pruning |
69
+ | R004 | Wildcard tables without `_TABLE_SUFFIX` | Scans every table in the dataset |
70
+ | R005 | The same query shape run over and over | One materialised refresh replaces all of them |
71
+ | R006 | Failed jobs that were still billed | Money that bought nothing |
72
+ | R007 | Cross joins / joins missing `ON` | Row multiplication |
73
+
74
+ ## Every dollar is counted once
75
+
76
+ A repeated `SELECT * ... LIMIT` matches three rules at once. If each rule
77
+ counted it, the reported savings would add up to more than your actual bill —
78
+ and the first thing a sceptical engineer does is add up our numbers.
79
+
80
+ So rules run in priority order and each job is claimed by exactly one rule,
81
+ the one with the most specific fix. The sum of findings can never exceed
82
+ actual spend, and there is a test that fails the build if it ever does.
83
+
84
+ Savings estimates are deliberately conservative. bqlens counts what a stated
85
+ fix would plausibly recover, not the full cost of the query. If it says $500,
86
+ the intent is that you find $500, not that you find $180 and stop trusting it.
87
+
88
+ ## Usage
89
+
90
+ ```bash
91
+ # Basic scan
92
+ bqlens scan --project my-project
93
+
94
+ # Non-US region (the JOBS view is region-qualified)
95
+ bqlens scan --project my-project --region region-asia-south1
96
+
97
+ # Your negotiated rate rather than list price
98
+ bqlens scan --project my-project --price-per-tib 5.00
99
+
100
+ # A page you can forward to your manager
101
+ bqlens scan --project my-project --format html --out report.html
102
+
103
+ # Machine-readable
104
+ bqlens scan --project my-project --format json
105
+
106
+ # Fail CI if waste creeps above a threshold
107
+ bqlens scan --project my-project --fail-over 200
108
+ ```
109
+
110
+ ### Options
111
+
112
+ | Flag | Default | Meaning |
113
+ |------|---------|---------|
114
+ | `--project` | — | GCP project to scan (required unless `--demo`) |
115
+ | `--region` | `region-us` | Region of the JOBS view |
116
+ | `--days` | `30` | Days of history |
117
+ | `--price-per-tib` | `6.25` | On-demand price per TiB scanned |
118
+ | `--min-gib` | `1.0` | Ignore queries smaller than this |
119
+ | `--min-repeats` | `10` | Repeats before a shape is flagged |
120
+ | `--format` | `terminal` | `terminal`, `json`, or `html` |
121
+ | `--out` | — | Write to a file |
122
+ | `--fail-over` | — | Exit 1 if monthly recoverable exceeds this |
123
+ | `--demo` | — | Run on synthetic data, no credentials |
124
+
125
+ ## Permissions
126
+
127
+ The scanning account needs `bigquery.jobs.listAll` on the project — included
128
+ in `roles/bigquery.resourceAdmin`, or grant it directly. Without `listAll` you
129
+ will only see your own jobs, not the whole project's.
130
+
131
+ The metadata query is itself capped at 10 GiB billed, so the scanner can never
132
+ become the expensive thing in your bill.
133
+
134
+ ## Install
135
+
136
+ ```bash
137
+ pip install bqlens # CLI + demo mode
138
+ pip install 'bqlens[bigquery]' # adds the BigQuery client for real scans
139
+ ```
140
+
141
+ Python 3.10+. Authenticate with `gcloud auth application-default login`.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ pip install -e '.[dev]'
147
+ pytest
148
+ ```
149
+
150
+ ## Limitations, stated plainly
151
+
152
+ - Rules work on query text and job metadata. bqlens does not read table schemas,
153
+ so it cannot know whether a table is partitioned — R003 flags unfiltered scans
154
+ as a *candidate*, not a certainty.
155
+ - Savings percentages per rule are heuristics based on typical wide analytics
156
+ tables. Your mileage varies.
157
+ - Slot-based (capacity) pricing is not yet modelled; cost figures assume
158
+ on-demand. Reservation support is planned.
159
+ - SQL parsing is regex-based, not a full parser. Exotic queries may be missed.
160
+
161
+ ## Licence
162
+
163
+ Apache-2.0
bqlens-0.1.0/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # bqlens
2
+
3
+ **Find wasted BigQuery spend in one command.**
4
+
5
+ No signup. No sales call. No agent installed in your VPC. It reads your query
6
+ history, tells you where the money is going, and tells you how to stop it.
7
+
8
+ ```bash
9
+ pip install bqlens
10
+ bqlens scan --project my-project --days 30
11
+ ```
12
+
13
+ ```
14
+ bqlens — my-project
15
+ 1,833 query jobs over the last 30 days
16
+
17
+ Spend in window $ 1,946.10
18
+ Projected monthly $ 1,946.10
19
+ Recoverable $ 572.50/mo (29% of spend)
20
+
21
+ 1. [HIGH] Wildcard table scans without _TABLE_SUFFIX (R004)
22
+ $179.30/mo recoverable · 22 jobs
23
+ ...
24
+ ```
25
+
26
+ Try it with no credentials at all:
27
+
28
+ ```bash
29
+ bqlens scan --demo
30
+ ```
31
+
32
+ ---
33
+
34
+ ## What it looks at
35
+
36
+ bqlens reads `INFORMATION_SCHEMA.JOBS_BY_PROJECT` — job metadata and the SQL
37
+ text of your queries.
38
+
39
+ **It never reads a row of your table data.** There is no row access, no
40
+ sampling, no data leaving your project. The whole tool is a read of your own
41
+ query log plus regex and arithmetic, running on your machine.
42
+
43
+ ## What it finds
44
+
45
+ | Rule | What it catches | Why it costs money |
46
+ |------|-----------------|--------------------|
47
+ | R001 | `SELECT *` on large scans | BigQuery bills per column read |
48
+ | R002 | `LIMIT` with no `WHERE` | LIMIT caps rows returned, not bytes scanned |
49
+ | R003 | Large scans with no filter | No partition pruning |
50
+ | R004 | Wildcard tables without `_TABLE_SUFFIX` | Scans every table in the dataset |
51
+ | R005 | The same query shape run over and over | One materialised refresh replaces all of them |
52
+ | R006 | Failed jobs that were still billed | Money that bought nothing |
53
+ | R007 | Cross joins / joins missing `ON` | Row multiplication |
54
+
55
+ ## Every dollar is counted once
56
+
57
+ A repeated `SELECT * ... LIMIT` matches three rules at once. If each rule
58
+ counted it, the reported savings would add up to more than your actual bill —
59
+ and the first thing a sceptical engineer does is add up our numbers.
60
+
61
+ So rules run in priority order and each job is claimed by exactly one rule,
62
+ the one with the most specific fix. The sum of findings can never exceed
63
+ actual spend, and there is a test that fails the build if it ever does.
64
+
65
+ Savings estimates are deliberately conservative. bqlens counts what a stated
66
+ fix would plausibly recover, not the full cost of the query. If it says $500,
67
+ the intent is that you find $500, not that you find $180 and stop trusting it.
68
+
69
+ ## Usage
70
+
71
+ ```bash
72
+ # Basic scan
73
+ bqlens scan --project my-project
74
+
75
+ # Non-US region (the JOBS view is region-qualified)
76
+ bqlens scan --project my-project --region region-asia-south1
77
+
78
+ # Your negotiated rate rather than list price
79
+ bqlens scan --project my-project --price-per-tib 5.00
80
+
81
+ # A page you can forward to your manager
82
+ bqlens scan --project my-project --format html --out report.html
83
+
84
+ # Machine-readable
85
+ bqlens scan --project my-project --format json
86
+
87
+ # Fail CI if waste creeps above a threshold
88
+ bqlens scan --project my-project --fail-over 200
89
+ ```
90
+
91
+ ### Options
92
+
93
+ | Flag | Default | Meaning |
94
+ |------|---------|---------|
95
+ | `--project` | — | GCP project to scan (required unless `--demo`) |
96
+ | `--region` | `region-us` | Region of the JOBS view |
97
+ | `--days` | `30` | Days of history |
98
+ | `--price-per-tib` | `6.25` | On-demand price per TiB scanned |
99
+ | `--min-gib` | `1.0` | Ignore queries smaller than this |
100
+ | `--min-repeats` | `10` | Repeats before a shape is flagged |
101
+ | `--format` | `terminal` | `terminal`, `json`, or `html` |
102
+ | `--out` | — | Write to a file |
103
+ | `--fail-over` | — | Exit 1 if monthly recoverable exceeds this |
104
+ | `--demo` | — | Run on synthetic data, no credentials |
105
+
106
+ ## Permissions
107
+
108
+ The scanning account needs `bigquery.jobs.listAll` on the project — included
109
+ in `roles/bigquery.resourceAdmin`, or grant it directly. Without `listAll` you
110
+ will only see your own jobs, not the whole project's.
111
+
112
+ The metadata query is itself capped at 10 GiB billed, so the scanner can never
113
+ become the expensive thing in your bill.
114
+
115
+ ## Install
116
+
117
+ ```bash
118
+ pip install bqlens # CLI + demo mode
119
+ pip install 'bqlens[bigquery]' # adds the BigQuery client for real scans
120
+ ```
121
+
122
+ Python 3.10+. Authenticate with `gcloud auth application-default login`.
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ pip install -e '.[dev]'
128
+ pytest
129
+ ```
130
+
131
+ ## Limitations, stated plainly
132
+
133
+ - Rules work on query text and job metadata. bqlens does not read table schemas,
134
+ so it cannot know whether a table is partitioned — R003 flags unfiltered scans
135
+ as a *candidate*, not a certainty.
136
+ - Savings percentages per rule are heuristics based on typical wide analytics
137
+ tables. Your mileage varies.
138
+ - Slot-based (capacity) pricing is not yet modelled; cost figures assume
139
+ on-demand. Reservation support is planned.
140
+ - SQL parsing is regex-based, not a full parser. Exotic queries may be missed.
141
+
142
+ ## Licence
143
+
144
+ Apache-2.0
@@ -0,0 +1,3 @@
1
+ """bqlens — find wasted BigQuery spend in one command."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,135 @@
1
+ """Command line interface for bqlens."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .models import DEFAULT_PRICE_PER_TIB, ScanResult
11
+ from .report import render_html, render_json, render_terminal
12
+ from .rules import run_all
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="bqlens",
18
+ description="Find wasted BigQuery spend. Read-only; never touches your table data.",
19
+ )
20
+ parser.add_argument("--version", action="version", version=f"bqlens {__version__}")
21
+ sub = parser.add_subparsers(dest="command", required=True)
22
+
23
+ scan = sub.add_parser("scan", help="Scan query history for recoverable spend")
24
+ scan.add_argument("--project", help="GCP project ID to scan")
25
+ scan.add_argument(
26
+ "--region",
27
+ default="region-us",
28
+ help="BigQuery region of the JOBS view (default: region-us)",
29
+ )
30
+ scan.add_argument("--days", type=int, default=30, help="Days of history (default: 30)")
31
+ scan.add_argument(
32
+ "--price-per-tib",
33
+ type=float,
34
+ default=DEFAULT_PRICE_PER_TIB,
35
+ help=f"On-demand price per TiB scanned (default: {DEFAULT_PRICE_PER_TIB})",
36
+ )
37
+ scan.add_argument(
38
+ "--min-gib",
39
+ type=float,
40
+ default=1.0,
41
+ help="Ignore queries smaller than this many GiB (default: 1.0)",
42
+ )
43
+ scan.add_argument(
44
+ "--min-repeats",
45
+ type=int,
46
+ default=10,
47
+ help="Repeats before a query shape is flagged for materialisation (default: 10)",
48
+ )
49
+ scan.add_argument(
50
+ "--format",
51
+ choices=["terminal", "json", "html"],
52
+ default="terminal",
53
+ help="Output format (default: terminal)",
54
+ )
55
+ scan.add_argument("--out", help="Write output to this file instead of stdout")
56
+ scan.add_argument("--no-color", action="store_true", help="Disable ANSI colour")
57
+ scan.add_argument(
58
+ "--demo",
59
+ action="store_true",
60
+ help="Run against built-in synthetic history. No credentials needed.",
61
+ )
62
+ scan.add_argument(
63
+ "--fail-over",
64
+ type=float,
65
+ default=None,
66
+ metavar="USD",
67
+ help="Exit non-zero if monthly recoverable spend exceeds this. For CI.",
68
+ )
69
+ return parser
70
+
71
+
72
+ def cmd_scan(args: argparse.Namespace) -> int:
73
+ if args.demo:
74
+ from .demo import generate
75
+
76
+ jobs = generate()
77
+ project = "demo-project"
78
+ else:
79
+ if not args.project:
80
+ print("error: --project is required (or use --demo)", file=sys.stderr)
81
+ return 2
82
+ from .fetch import fetch_jobs
83
+
84
+ jobs = fetch_jobs(project=args.project, region=args.region, days=args.days)
85
+ project = args.project
86
+
87
+ if not jobs:
88
+ print("No query jobs found in that window.", file=sys.stderr)
89
+ return 0
90
+
91
+ min_bytes = int(args.min_gib * 1024**3)
92
+ findings = run_all(
93
+ jobs,
94
+ price_per_tib=args.price_per_tib,
95
+ min_bytes=min_bytes,
96
+ min_repeats=args.min_repeats,
97
+ )
98
+
99
+ result = ScanResult(
100
+ findings=findings,
101
+ total_jobs=len(jobs),
102
+ total_spend_usd=sum(j.cost(args.price_per_tib) for j in jobs),
103
+ days=args.days,
104
+ project=project,
105
+ price_per_tib=args.price_per_tib,
106
+ )
107
+
108
+ if args.format == "json":
109
+ output = render_json(result)
110
+ elif args.format == "html":
111
+ output = render_html(result)
112
+ else:
113
+ colour = sys.stdout.isatty() and not args.no_color
114
+ output = render_terminal(result, colour=colour)
115
+
116
+ if args.out:
117
+ Path(args.out).write_text(output, encoding="utf-8")
118
+ print(f"Wrote {args.out}", file=sys.stderr)
119
+ else:
120
+ print(output)
121
+
122
+ if args.fail_over is not None and result.monthly_recoverable_usd > args.fail_over:
123
+ return 1
124
+ return 0
125
+
126
+
127
+ def main(argv: list[str] | None = None) -> int:
128
+ args = build_parser().parse_args(argv)
129
+ if args.command == "scan":
130
+ return cmd_scan(args)
131
+ return 2
132
+
133
+
134
+ if __name__ == "__main__":
135
+ raise SystemExit(main())
@@ -0,0 +1,188 @@
1
+ """Synthetic job history so the scanner can be tried without credentials.
2
+
3
+ `bqlens scan --demo` runs the full pipeline against this. It is also what the
4
+ test suite asserts against, so the numbers below are fixtures, not decoration.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import random
10
+ from datetime import datetime, timedelta
11
+
12
+ from .models import Job
13
+
14
+ GIB = 1024**3
15
+
16
+ _USERS = [
17
+ "analytics@example.com",
18
+ "dbt-runner@example.iam.gserviceaccount.com",
19
+ "looker@example.iam.gserviceaccount.com",
20
+ "priya@example.com",
21
+ ]
22
+
23
+
24
+ def _job(
25
+ idx: int,
26
+ query: str,
27
+ gib: float,
28
+ user: str,
29
+ *,
30
+ cache_hit: bool = False,
31
+ error: str | None = None,
32
+ when: datetime | None = None,
33
+ ) -> Job:
34
+ billed = int(gib * GIB)
35
+ return Job(
36
+ job_id=f"demo_job_{idx:05d}",
37
+ user_email=user,
38
+ creation_time=when or (datetime.utcnow() - timedelta(hours=idx % 720)),
39
+ query=query,
40
+ total_bytes_billed=0 if cache_hit else billed,
41
+ total_bytes_processed=billed,
42
+ cache_hit=cache_hit,
43
+ statement_type="SELECT",
44
+ referenced_tables=["demo-project.warehouse.events"],
45
+ total_slot_ms=int(gib * 1200),
46
+ error_result=error,
47
+ )
48
+
49
+
50
+ def generate(seed: int = 7) -> list[Job]:
51
+ """A plausible 30 days of query history for a mid-sized data team."""
52
+ rng = random.Random(seed)
53
+ jobs: list[Job] = []
54
+ idx = 0
55
+
56
+ # 1. A Looker dashboard hammering the same unaggregated query all month.
57
+ dashboard_sql = """
58
+ SELECT user_id, event_name, event_timestamp, platform, country,
59
+ session_id, revenue_usd
60
+ FROM `demo-project.warehouse.events`
61
+ WHERE event_date >= '2026-08-01'
62
+ """
63
+ for _ in range(280):
64
+ idx += 1
65
+ jobs.append(_job(idx, dashboard_sql, rng.uniform(7.5, 9.0), "looker@example.iam.gserviceaccount.com"))
66
+
67
+ # 2. GA4-style wildcard scans with no _TABLE_SUFFIX filter. The classic.
68
+ wildcard_sql = """
69
+ SELECT event_name, COUNT(*) AS n
70
+ FROM `demo-project.analytics_311.events_*`
71
+ GROUP BY event_name
72
+ """
73
+ for _ in range(22):
74
+ idx += 1
75
+ jobs.append(_job(idx, wildcard_sql, rng.uniform(140, 190), "analytics@example.com"))
76
+
77
+ # 3. Analysts exploring with SELECT * LIMIT, believing LIMIT is cheap.
78
+ for _ in range(46):
79
+ idx += 1
80
+ jobs.append(
81
+ _job(
82
+ idx,
83
+ "SELECT * FROM `demo-project.warehouse.events` LIMIT 100",
84
+ rng.uniform(58, 72),
85
+ "priya@example.com",
86
+ )
87
+ )
88
+
89
+ # 4. SELECT * feeding a transform that uses four columns.
90
+ for _ in range(35):
91
+ idx += 1
92
+ jobs.append(
93
+ _job(
94
+ idx,
95
+ "SELECT * FROM `demo-project.warehouse.transactions` "
96
+ "WHERE txn_date BETWEEN '2026-08-01' AND '2026-08-31'",
97
+ rng.uniform(18, 26),
98
+ "dbt-runner@example.iam.gserviceaccount.com",
99
+ )
100
+ )
101
+
102
+ # 5. A broken scheduled query retrying all month and billing every time.
103
+ for _ in range(64):
104
+ idx += 1
105
+ jobs.append(
106
+ _job(
107
+ idx,
108
+ "SELECT customer_id, SUM(amount) FROM `demo-project.warehouse.ledger` "
109
+ "WHERE posted_date >= '2026-08-01' GROUP BY 1",
110
+ rng.uniform(11, 15),
111
+ "dbt-runner@example.iam.gserviceaccount.com",
112
+ error="Not found: Table demo-project:warehouse.ledger_v2",
113
+ )
114
+ )
115
+
116
+ # 6. An unfiltered full scan somebody schedules nightly.
117
+ for _ in range(30):
118
+ idx += 1
119
+ jobs.append(
120
+ _job(
121
+ idx,
122
+ "SELECT customer_id, lifetime_value, segment "
123
+ "FROM `demo-project.warehouse.customer_360`",
124
+ rng.uniform(34, 44),
125
+ "analytics@example.com",
126
+ )
127
+ )
128
+
129
+ # 7. A join missing its ON clause.
130
+ for _ in range(6):
131
+ idx += 1
132
+ jobs.append(
133
+ _job(
134
+ idx,
135
+ "SELECT a.user_id, b.campaign_id "
136
+ "FROM `demo-project.warehouse.users` a "
137
+ "JOIN `demo-project.warehouse.campaigns` b "
138
+ "WHERE a.country = 'IN'",
139
+ rng.uniform(25, 33),
140
+ "priya@example.com",
141
+ )
142
+ )
143
+
144
+ # 8. Plenty of ordinary, well-written, cheap queries — the healthy majority.
145
+ for _ in range(900):
146
+ idx += 1
147
+ col = rng.choice(["event_name", "platform", "country"])
148
+ jobs.append(
149
+ _job(
150
+ idx,
151
+ f"SELECT {col}, COUNT(*) FROM `demo-project.warehouse.events` "
152
+ f"WHERE event_date = '2026-09-{rng.randint(1, 17):02d}' GROUP BY 1",
153
+ rng.uniform(0.05, 0.9),
154
+ rng.choice(_USERS),
155
+ )
156
+ )
157
+
158
+ # 9. Legitimate heavy analytical work: large, well-written, distinct
159
+ # queries. This is the bulk of a healthy bill and nothing should flag
160
+ # it. Without this block the demo would claim an absurd savings rate.
161
+ _dims = ["country", "platform", "channel", "device_type", "cohort", "tier", "region"]
162
+ _metrics = ["revenue_usd", "sessions", "orders", "refunds", "margin_usd", "units"]
163
+ _tables = ["transactions", "sessions_daily", "orders", "subscriptions", "inventory_daily"]
164
+ for i in range(300):
165
+ idx += 1
166
+ dim = _dims[i % len(_dims)]
167
+ metric = _metrics[(i // 7) % len(_metrics)]
168
+ table = _tables[(i // 3) % len(_tables)]
169
+ jobs.append(
170
+ _job(
171
+ idx,
172
+ f"SELECT {dim}, DATE_TRUNC(txn_date, MONTH) AS m, SUM({metric}) AS total_{i} "
173
+ f"FROM `demo-project.warehouse.{table}` t "
174
+ f"JOIN `demo-project.warehouse.dim_customer` c ON t.customer_id = c.customer_id "
175
+ f"WHERE txn_date >= '2026-0{rng.randint(1, 8)}-01' "
176
+ f"GROUP BY 1, 2",
177
+ rng.uniform(40, 90),
178
+ rng.choice(_USERS),
179
+ )
180
+ )
181
+
182
+ # 10. Cache hits — free, and correctly ignored by every rule.
183
+ for _ in range(150):
184
+ idx += 1
185
+ jobs.append(_job(idx, dashboard_sql, 8.0, "looker@example.iam.gserviceaccount.com", cache_hit=True))
186
+
187
+ rng.shuffle(jobs)
188
+ return jobs