Skip to content

Commit 27f6500

Browse files
authored
Merge pull request #2959 from mabel-dev/#2958
Axis Error on DISTINCT
2 parents 26d7e17 + e38f7ca commit 27f6500

105 files changed

Lines changed: 393 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env python3
2+
"""
3+
One-off script to generate fake security finding Parquet files.
4+
5+
Creates a number of Parquet files (default 100) each containing a number of
6+
fake security finding records (default 100). The fields are:
7+
8+
- id (monotonically increasing integer)
9+
- server (string)
10+
- patch_id (string, like MS KB item)
11+
- cves (list of CVE strings)
12+
- first_found (datetime)
13+
- times_found (integer)
14+
- risk_score (double between 0.0 and 10.0)
15+
16+
Usage:
17+
python dev/generate_security_parquet_files.py --count 100 --per-file 100 --out ./dev/security_parquets
18+
19+
This script uses pandas and pyarrow. Install:
20+
pip install pandas pyarrow numpy
21+
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import argparse
27+
import random
28+
from datetime import datetime
29+
from datetime import timedelta
30+
from datetime import timezone
31+
from pathlib import Path
32+
33+
import numpy as np
34+
import pandas as pd
35+
import pyarrow as pa
36+
import pyarrow.parquet as pq
37+
38+
39+
def random_server(idx: int) -> str:
40+
# Basename and a shard number so it's easy to find duplicates
41+
names = [
42+
"alpha", "beta", "gamma", "delta", "omega", "lambda", "zeta", "theta",
43+
"sigma", "kappa",
44+
]
45+
return f"{random.choice(names)}-srv-{idx % 100}"
46+
47+
48+
def random_patch_id() -> str:
49+
# Windows KB-style string: KB followed by 5-7 digits
50+
return f"KB{random.randint(10000, 9999999)}"
51+
52+
53+
def random_cves() -> list[str]:
54+
count = random.randint(1, 3)
55+
cves = []
56+
for _ in range(count):
57+
year = random.randint(2017, datetime.now().year)
58+
# NNNN to NNNNNN
59+
number = random.randint(1000, 999999)
60+
cves.append(f"CVE-{year}-{number}")
61+
return cves
62+
63+
64+
def random_first_found() -> datetime:
65+
# Random datetime in the last 3 years
66+
now = datetime.now(timezone.utc)
67+
days = random.randint(0, 3 * 365)
68+
seconds = random.randint(0, 24 * 60 * 60 - 1)
69+
return now - timedelta(days=days, seconds=seconds)
70+
71+
72+
def random_times_found() -> int:
73+
# Use Poisson-like distribution to bias to small numbers but allow larger ones
74+
lam = 2.5
75+
# numpy.poisson returns ints >= 0
76+
n = np.random.poisson(lam)
77+
return int(n)
78+
79+
80+
def random_risk_score() -> float:
81+
# Use a skewed distribution so most are lower risk but some are high
82+
# uniform then square root to bias towards lower numbers
83+
r = random.random()
84+
return round(10.0 * (r ** 0.7), 3) # 0..10
85+
86+
87+
def gen_record(rec_id: int) -> dict:
88+
ts = random_first_found()
89+
return {
90+
"id": rec_id,
91+
"server": random_server(rec_id),
92+
"patch_id": random_patch_id(),
93+
"cves": random_cves(),
94+
"first_found": ts,
95+
"times_found": random_times_found(),
96+
"risk_score": random_risk_score(),
97+
}
98+
99+
100+
def make_file(file_path: Path, start_id: int, n_records: int, seed: int | None = None):
101+
if seed is not None:
102+
random.seed(seed + start_id)
103+
np.random.seed(seed + start_id)
104+
105+
ids = []
106+
servers = []
107+
patch_ids = []
108+
cves_list = []
109+
first_found_list = []
110+
times_found_list = []
111+
risk_scores = []
112+
113+
rec_id = start_id
114+
for i in range(n_records):
115+
r = gen_record(rec_id)
116+
ids.append(r["id"])
117+
servers.append(r["server"])
118+
patch_ids.append(r["patch_id"])
119+
cves_list.append(r["cves"])
120+
first_found_list.append(r["first_found"])
121+
times_found_list.append(r["times_found"])
122+
risk_scores.append(r["risk_score"])
123+
rec_id += 1
124+
125+
# Build pyarrow arrays with types
126+
arr_id = pa.array(ids, type=pa.int64())
127+
arr_server = pa.array(servers, type=pa.string())
128+
arr_patch = pa.array(patch_ids, type=pa.string())
129+
arr_cves = pa.array(cves_list, type=pa.list_(pa.string()))
130+
arr_first = pa.array(first_found_list, type=pa.timestamp("ms", tz="UTC"))
131+
arr_times = pa.array(times_found_list, type=pa.int64())
132+
arr_risk = pa.array(risk_scores, type=pa.float64())
133+
134+
# Build table ensuring specific schema
135+
table = pa.Table.from_arrays(
136+
[arr_id, arr_server, arr_patch, arr_cves, arr_first, arr_times, arr_risk],
137+
names=["id", "server", "patch_id", "cves", "first_found", "times_found", "risk_score"],
138+
)
139+
140+
# Write parquet
141+
pq.write_table(table, file_path, use_deprecated_int96_timestamps=False)
142+
return rec_id - start_id # number of records written
143+
144+
145+
def main():
146+
parser = argparse.ArgumentParser(description="Generate Parquet files with fake security findings")
147+
parser.add_argument("--count", type=int, default=100, help="Number of files to create (default 100)")
148+
parser.add_argument("--per-file", type=int, default=100, help="Records per file (default 100)")
149+
parser.add_argument("--out", type=str, default="dev/security_parquets", help="Output directory")
150+
parser.add_argument("--seed", type=int, default=42, help="Random seed for reproducibility")
151+
parser.add_argument("--prefix", type=str, default="security_findings", help="Filename prefix")
152+
153+
args = parser.parse_args()
154+
155+
outdir = Path(args.out)
156+
outdir.mkdir(parents=True, exist_ok=True)
157+
158+
file_count = args.count
159+
per_file = args.per_file
160+
161+
seed = args.seed
162+
163+
next_id = 1
164+
print(f"Generating {file_count} parquet files with {per_file} records each: {file_count * per_file} records total")
165+
print("Writing to:", outdir)
166+
167+
for i in range(file_count):
168+
fname = f"{args.prefix}-{i:04d}.parquet"
169+
path = outdir / fname
170+
# Pass a slight offset to seed so each file is different but reproducible
171+
wrote = make_file(path, start_id=next_id, n_records=per_file, seed=seed)
172+
next_id += wrote
173+
if (i + 1) % 10 == 0 or i == file_count - 1:
174+
print(f" - {i+1}/{file_count} written (last id {next_id - 1})")
175+
176+
print("Done.")
177+
178+
179+
if __name__ == "__main__":
180+
main()

opteryx/__version__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# THIS FILE IS AUTOMATICALLY UPDATED DURING THE BUILD PROCESS
22
# DO NOT EDIT THIS FILE DIRECTLY
33

4-
__build__ = 1954
4+
__build__ = 1964
55
__author__ = "@joocer"
6-
__version__ = "0.26.2-beta.1954"
6+
__version__ = "0.26.2-beta.1964"
77

88
# Store the version here so:
99
# 1) we don't load dependencies by storing it in __init__.py

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "opteryx"
3-
version = "0.26.2-beta.1954"
3+
version = "0.26.2-beta.1964"
44
description = "Query your data, where it lives"
55
requires-python = '>=3.11'
66
readme = {file = "README.md", content-type = "text/markdown"}
8.17 KB
Binary file not shown.
8.39 KB
Binary file not shown.
8.13 KB
Binary file not shown.
8.2 KB
Binary file not shown.
8.07 KB
Binary file not shown.
8.16 KB
Binary file not shown.
8.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)