sqljev 0.1.0__py3-none-any.whl
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.
- sqljev/__init__.py +6 -0
- sqljev/__main__.py +3 -0
- sqljev/aws_lambda.py +16 -0
- sqljev/cli.py +516 -0
- sqljev/core.py +674 -0
- sqljev/demo.py +307 -0
- sqljev/duckdb.py +56 -0
- sqljev/finetune.py +367 -0
- sqljev/gateway.py +215 -0
- sqljev/spark.py +64 -0
- sqljev-0.1.0.dist-info/METADATA +450 -0
- sqljev-0.1.0.dist-info/RECORD +16 -0
- sqljev-0.1.0.dist-info/WHEEL +4 -0
- sqljev-0.1.0.dist-info/entry_points.txt +2 -0
- sqljev-0.1.0.dist-info/licenses/LICENSE +176 -0
- sqljev-0.1.0.dist-info/licenses/NOTICE +36 -0
sqljev/demo.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Demo data: a 100,000-row, 10-domain set of realistic tables with exact ground-truth labels.
|
|
3
|
+
|
|
4
|
+
python -m sqljev.demo --rows-per-table 10000 --out bench/bench.duckdb # the benchmark (bench/run.py)
|
|
5
|
+
from sqljev.demo import TASKS, generate # in a notebook
|
|
6
|
+
|
|
7
|
+
Each table is generated from parts whose meaning is known, so labels are exact. Slots (names, amounts, dates,
|
|
8
|
+
products, codes) make rows distinct; a few percent are exact repeats, as in real tables. Every task includes
|
|
9
|
+
hard negatives (a neutral complaint is not angry, a hospital *visit* for tests is not a serious event, ...).
|
|
10
|
+
|
|
11
|
+
TASKS below is the benchmark: table, the columns the model may see (never the label), the plain-language
|
|
12
|
+
question, its kind and options, and the label column.
|
|
13
|
+
"""
|
|
14
|
+
import argparse
|
|
15
|
+
import random
|
|
16
|
+
|
|
17
|
+
TASKS = [
|
|
18
|
+
# name, table, columns shown to the model, question, kind, options, label column
|
|
19
|
+
("ticket_angry", "tickets", ["subject", "body"], "the customer is angry", "noul", None, "angry"),
|
|
20
|
+
("ticket_team", "tickets", ["subject", "body"], "which team should handle this ticket?", "choice",
|
|
21
|
+
["billing", "technical", "sales", "security"], "team"),
|
|
22
|
+
("review_sentiment", "reviews", ["title", "text"], "how does the reviewer feel about the product?", "score",
|
|
23
|
+
["negative", "mixed", "positive"], "sentiment"),
|
|
24
|
+
("review_defect", "reviews", ["title", "text"], "the review reports a defective or broken product", "noul", None,
|
|
25
|
+
"defect"),
|
|
26
|
+
("ae_serious", "adverse_events", ["drug", "narrative"],
|
|
27
|
+
"the adverse event was serious: the patient died, was hospitalised, or it was life-threatening", "noul", None,
|
|
28
|
+
"serious"),
|
|
29
|
+
("ae_system", "adverse_events", ["drug", "narrative"], "which body system does the adverse event affect?",
|
|
30
|
+
"choice", ["liver", "heart", "skin", "nervous system", "digestive system"], "body_system"),
|
|
31
|
+
("txn_category", "transactions", ["merchant", "memo"], "what kind of business expense is this?", "choice",
|
|
32
|
+
["travel", "meals", "software", "office supplies", "entertainment"], "category"),
|
|
33
|
+
("job_seniority", "job_posts", ["title", "description"], "how senior is this role?", "score",
|
|
34
|
+
["junior", "mid-level", "senior"], "seniority"),
|
|
35
|
+
("job_remote", "job_posts", ["title", "description"], "the job can be done fully remotely", "noul", None, "remote"),
|
|
36
|
+
("listing_pets", "listings", ["headline", "description"], "pets are allowed in this rental", "noul", None, "pets"),
|
|
37
|
+
("clause_type", "clauses", ["text"], "what type of contract clause is this?", "choice",
|
|
38
|
+
["termination", "liability", "confidentiality", "payment", "intellectual property"], "clause_type"),
|
|
39
|
+
("email_guarantee", "emails", ["subject", "body"], "the email promises or guarantees investment returns",
|
|
40
|
+
"noul", None, "violation"),
|
|
41
|
+
("company_match", "company_pairs", ["record_a", "record_b"], "record_a and record_b are the same company",
|
|
42
|
+
"noul", None, "same"),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
FIRST = ["Maria", "John", "Aisha", "Wei", "Carlos", "Priya", "Tom", "Fatima", "Lukas", "Yuki", "Olga", "Samuel",
|
|
46
|
+
"Chloe", "Ahmed", "Ingrid", "Diego", "Nora", "Raj", "Emma", "Kofi"]
|
|
47
|
+
LAST = ["Garcia", "Smith", "Khan", "Zhang", "Rossi", "Patel", "Nguyen", "Muller", "Kowalski", "Silva", "Brown",
|
|
48
|
+
"Tanaka", "Okafor", "Larsen", "Haddad", "Novak", "Moreau", "Kim"]
|
|
49
|
+
MONTHS = ["March", "April", "May", "June", "July", "August"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pick(rng, xs):
|
|
53
|
+
return rng.choice(xs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------ tickets
|
|
57
|
+
T_ISSUE = {
|
|
58
|
+
"billing": ["I was charged {amt} twice for order {order}.", "My invoice {inv} shows {amt}, more than my plan.",
|
|
59
|
+
"The refund of {amt} promised on {date} never arrived.", "I cancelled but you still billed me {amt}."],
|
|
60
|
+
"technical": ["The app crashes when I open settings, error {err}.", "Since {date} I cannot log in: {err}.",
|
|
61
|
+
"Data export fails halfway with {err}.", "Pages take 30 seconds to load since the update."],
|
|
62
|
+
"sales": ["What would 40 seats cost per year?", "Can we get a quote for our second office?",
|
|
63
|
+
"Is there a discount for annual billing?", "Could someone demo the Enterprise plan to us?"],
|
|
64
|
+
"security": ["I got a login alert from a country I have never visited.", "Someone changed my account email.",
|
|
65
|
+
"We leaked an API key in a public repo, please revoke it.", "2FA codes arrive for logins I did not make."],
|
|
66
|
+
}
|
|
67
|
+
T_TONE = {
|
|
68
|
+
True: ["This is unacceptable and I am furious.", "Fix it TODAY or I cancel and tell everyone.",
|
|
69
|
+
"I am sick of your useless support.", "WHY IS THIS STILL BROKEN? I AM DONE.",
|
|
70
|
+
"You people are incompetent, I have wasted hours."],
|
|
71
|
+
False: ["Thanks a lot for your help!", "No rush.", "Kind regards.", "This is the second time, please fix it.",
|
|
72
|
+
"Could you check and get back to me?", "I would like this resolved soon."],
|
|
73
|
+
}
|
|
74
|
+
T_SUBJ = {"billing": ["Charge", "Invoice", "Refund"], "technical": ["Bug", "Crash", "Cannot log in"],
|
|
75
|
+
"sales": ["Pricing", "Quote", "Demo"], "security": ["Security", "Suspicious login", "Account access"]}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def tickets(rng, i):
|
|
79
|
+
team = rng.choices(list(T_ISSUE), [35, 35, 15, 15])[0]
|
|
80
|
+
angry = rng.random() < .3
|
|
81
|
+
slots = {"amt": "$%d.%02d" % (rng.randint(9, 4999), rng.randint(0, 99)), "order": "#%d" % rng.randint(100000, 999999),
|
|
82
|
+
"inv": "INV-%d" % rng.randint(10000, 99999), "date": "%s %d" % (pick(rng, MONTHS), rng.randint(1, 28)),
|
|
83
|
+
"err": pick(rng, ["E%d" % rng.randint(100, 999), "HTTP 500", "timeout"])}
|
|
84
|
+
body = " ".join([pick(rng, ["Hi,", "Hello,", ""]), pick(rng, T_ISSUE[team]).format(**slots), pick(rng, T_TONE[angry])]).strip()
|
|
85
|
+
return {"id": i, "subject": pick(rng, T_SUBJ[team]), "body": body, "angry": angry, "team": team}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------ product reviews
|
|
89
|
+
PRODUCTS = ["blender", "headphones", "desk lamp", "running shoes", "coffee grinder", "backpack", "phone case",
|
|
90
|
+
"air fryer", "keyboard", "yoga mat", "water bottle", "office chair"]
|
|
91
|
+
R_POS = ["Absolutely love it, works perfectly.", "Best purchase this year.", "Great quality for the price.",
|
|
92
|
+
"Exceeded my expectations, highly recommend."]
|
|
93
|
+
R_NEG = ["Complete waste of money.", "Very disappointed, returning it.", "Would not recommend to anyone.",
|
|
94
|
+
"Cheap and poorly made."]
|
|
95
|
+
R_MIX = ["It does the job but the design is clumsy.", "Good sound, but uncomfortable after an hour.",
|
|
96
|
+
"Nice looking, though it feels flimsy.", "Works fine, shipping took forever though."]
|
|
97
|
+
R_DEFECT = ["It stopped working after {n} days.", "Arrived with a cracked {part}.", "The {part} broke the first time I used it.",
|
|
98
|
+
"It overheats and shuts off.", "One of the buttons is dead out of the box."]
|
|
99
|
+
R_FINE = ["Arrived on time and well packed.", "Setup took two minutes.", "The {part} feels solid.", "Battery lasts all day."]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def reviews(rng, i):
|
|
103
|
+
sentiment = rng.choices(["negative", "mixed", "positive"], [30, 25, 45])[0]
|
|
104
|
+
defect = sentiment != "positive" and rng.random() < .55
|
|
105
|
+
prod, part = pick(rng, PRODUCTS), pick(rng, ["lid", "handle", "switch", "cable", "zipper", "base"])
|
|
106
|
+
tone = {"negative": R_NEG, "mixed": R_MIX, "positive": R_POS}[sentiment]
|
|
107
|
+
detail = pick(rng, R_DEFECT if defect else R_FINE).format(n=rng.randint(2, 40), part=part)
|
|
108
|
+
text = " ".join(rng.sample([pick(rng, tone), detail], 2))
|
|
109
|
+
stars = {"negative": pick(rng, ["1/5", "2/5"]), "mixed": "3/5", "positive": pick(rng, ["4/5", "5/5"])}[sentiment]
|
|
110
|
+
title = "%s: %s" % (prod.capitalize(), pick(rng, {"negative": ["Avoid", "Disappointing", "Meh"],
|
|
111
|
+
"mixed": ["Okay", "Mixed feelings", "Decent"],
|
|
112
|
+
"positive": ["Great", "Love it", "Excellent"]}[sentiment]))
|
|
113
|
+
return {"id": i, "product": prod, "title": title, "text": text, "stars_hidden": stars,
|
|
114
|
+
"sentiment": sentiment, "defect": defect}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------ pharmacovigilance: adverse event reports
|
|
118
|
+
DRUGS = ["atorvastatin", "metformin", "amoxicillin", "ibuprofen", "sertraline", "lisinopril", "omeprazole",
|
|
119
|
+
"levothyroxine", "warfarin", "gabapentin", "prednisone", "clopidogrel"]
|
|
120
|
+
AE = {
|
|
121
|
+
"liver": ["elevated liver enzymes (ALT {n}x ULN)", "jaundice and dark urine", "drug-induced hepatitis"],
|
|
122
|
+
"heart": ["palpitations and an irregular heartbeat", "QT prolongation on ECG", "chest pain with tachycardia"],
|
|
123
|
+
"skin": ["a widespread itchy rash", "hives on the arms and neck", "blistering skin lesions"],
|
|
124
|
+
"nervous system": ["severe dizziness and headache", "tingling and numbness in both feet", "a seizure"],
|
|
125
|
+
"digestive system": ["persistent nausea and vomiting", "severe abdominal pain and diarrhoea", "gastrointestinal bleeding"],
|
|
126
|
+
}
|
|
127
|
+
SERIOUS = ["Patient was admitted to hospital for {d} days.", "The event was considered life-threatening; ICU admission.",
|
|
128
|
+
"Patient died {d} days after onset.", "Required emergency hospitalisation and IV treatment."]
|
|
129
|
+
NONSERIOUS = ["Symptoms resolved after the drug was stopped.", "Managed as an outpatient; recovered fully.",
|
|
130
|
+
"Patient visited the hospital lab for routine tests; no admission.", "Mild; the dose was reduced and symptoms eased."]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def adverse_events(rng, i):
|
|
134
|
+
system = pick(rng, list(AE))
|
|
135
|
+
serious = rng.random() < .35
|
|
136
|
+
drug = pick(rng, DRUGS)
|
|
137
|
+
d = rng.randint(2, 21)
|
|
138
|
+
narrative = "A %d-year-old %s taking %s %dmg developed %s about %d days after starting treatment. %s" % (
|
|
139
|
+
rng.randint(18, 89), pick(rng, ["man", "woman"]), drug, pick(rng, [5, 10, 20, 40, 50, 100, 500]),
|
|
140
|
+
pick(rng, AE[system]).format(n=rng.randint(3, 12)), rng.randint(1, 60),
|
|
141
|
+
pick(rng, SERIOUS if serious else NONSERIOUS).format(d=d))
|
|
142
|
+
return {"id": i, "report_no": "AE-%06d" % rng.randint(0, 999999), "drug": drug, "narrative": narrative,
|
|
143
|
+
"serious": serious, "body_system": system}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------ expense transactions
|
|
147
|
+
TXN = {
|
|
148
|
+
"travel": [("Delta Air Lines", "flight {c1} to {c2}"), ("Marriott", "hotel {n} nights in {c2}"),
|
|
149
|
+
("Uber", "ride to the airport"), ("Hertz", "car rental in {c2}"), ("Amtrak", "train to {c2}")],
|
|
150
|
+
"meals": [("Olive Garden", "team lunch, {n} people"), ("Starbucks", "coffee with a client"),
|
|
151
|
+
("DoorDash", "dinner while working late"), ("Chipotle", "lunch at the offsite")],
|
|
152
|
+
"software": [("GitHub", "Team plan, {n} seats"), ("Atlassian", "Jira subscription"), ("AWS", "cloud hosting for {m}"),
|
|
153
|
+
("Figma", "design licences"), ("Slack", "annual workspace plan")],
|
|
154
|
+
"office supplies": [("Staples", "printer paper and toner"), ("IKEA", "desk and chair for the new hire"),
|
|
155
|
+
("Amazon", "USB-C cables and a keyboard"), ("Office Depot", "whiteboard markers and notebooks")],
|
|
156
|
+
"entertainment": [("Ticketmaster", "concert tickets for the client"), ("AMC Theatres", "team movie night"),
|
|
157
|
+
("TopGolf", "team outing"), ("StubHub", "basketball game with partners")],
|
|
158
|
+
}
|
|
159
|
+
CITIES = ["Boston", "Denver", "Austin", "Chicago", "Seattle", "Miami", "Atlanta"]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def transactions(rng, i):
|
|
163
|
+
cat = pick(rng, list(TXN))
|
|
164
|
+
merchant, memo = pick(rng, TXN[cat])
|
|
165
|
+
memo = memo.format(c1=pick(rng, CITIES), c2=pick(rng, CITIES), n=rng.randint(2, 12), m=pick(rng, MONTHS))
|
|
166
|
+
return {"id": i, "merchant": merchant.upper() + " " + str(rng.randint(100, 9999)), "memo": memo,
|
|
167
|
+
"amount": round(rng.uniform(4, 2400), 2), "category": cat}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------ job posts
|
|
171
|
+
ROLES = ["Software Engineer", "Data Analyst", "Product Designer", "Accountant", "Marketing Manager", "DevOps Engineer",
|
|
172
|
+
"Nurse", "Sales Representative"]
|
|
173
|
+
SEN = {"junior": (["Junior", "Graduate", "Entry-level"], "0-2 years of experience; we will train you."),
|
|
174
|
+
"mid-level": (["", "Mid-level"], "3-5 years of experience working independently."),
|
|
175
|
+
"senior": (["Senior", "Lead", "Principal"], "8+ years of experience; you will mentor the team and own the roadmap.")}
|
|
176
|
+
REMOTE = ["This role is 100% remote, work from anywhere in the EU.", "Fully remote; we never meet in an office.",
|
|
177
|
+
"Remote-first team, you choose where you work."]
|
|
178
|
+
ONSITE = ["You will work on site at our {c} office five days a week.", "Hybrid: three days a week in our {c} office.",
|
|
179
|
+
"On-site role at the {c} hospital.", "Occasional remote days, mostly in the {c} office."]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def job_posts(rng, i):
|
|
183
|
+
sen = pick(rng, list(SEN))
|
|
184
|
+
remote = rng.random() < .4
|
|
185
|
+
role = pick(rng, ROLES)
|
|
186
|
+
titles, exp = SEN[sen]
|
|
187
|
+
title = ("%s %s" % (pick(rng, titles), role)).strip()
|
|
188
|
+
desc = "%s %s %s Salary %s." % (
|
|
189
|
+
pick(rng, ["We are growing fast.", "Join our friendly team.", "Help us build the future of logistics."]), exp,
|
|
190
|
+
pick(rng, REMOTE if remote else ONSITE).format(c=pick(rng, CITIES)),
|
|
191
|
+
"$%dk-$%dk" % ((b := rng.randint(40, 180)), b + rng.randint(10, 40)))
|
|
192
|
+
return {"id": i, "title": title, "description": desc, "seniority": sen, "remote": remote}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------ rental listings
|
|
196
|
+
PETS_YES = ["Cats and dogs welcome!", "Pet-friendly building with a dog park.", "Small pets allowed with a deposit."]
|
|
197
|
+
PETS_NO = ["No pets, please.", "Sorry, the landlord does not allow animals.", "Pets are not permitted in this building.",
|
|
198
|
+
"Close to a large park, perfect for walks (no pets in the unit)."]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def listings(rng, i):
|
|
202
|
+
pets = rng.random() < .45
|
|
203
|
+
beds = rng.randint(1, 4)
|
|
204
|
+
desc = "%d-bedroom apartment in %s, %d sq ft, %s. %s %s" % (
|
|
205
|
+
beds, pick(rng, CITIES), rng.randint(450, 2200), pick(rng, ["newly renovated", "sunny corner unit", "quiet street",
|
|
206
|
+
"walk to the metro"]),
|
|
207
|
+
pick(rng, ["In-unit laundry.", "Balcony with a view.", "Gym in the building.", "Parking included."]),
|
|
208
|
+
pick(rng, PETS_YES if pets else PETS_NO))
|
|
209
|
+
return {"id": i, "headline": "%d BR in %s - $%d/mo" % (beds, pick(rng, CITIES), rng.randint(900, 5200)),
|
|
210
|
+
"description": desc, "pets": pets}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------ contract clauses
|
|
214
|
+
CLAUSES = {
|
|
215
|
+
"termination": ["Either party may terminate this Agreement upon {n} days' written notice.",
|
|
216
|
+
"This Agreement terminates automatically if the Customer becomes insolvent."],
|
|
217
|
+
"liability": ["In no event shall either party's aggregate liability exceed the fees paid in the prior {n} months.",
|
|
218
|
+
"Neither party is liable for indirect, incidental or consequential damages."],
|
|
219
|
+
"confidentiality": ["Each party shall keep the other's Confidential Information secret for {n} years.",
|
|
220
|
+
"The Recipient shall not disclose Confidential Information to any third party."],
|
|
221
|
+
"payment": ["Invoices are payable within {n} days of receipt; late amounts accrue 1.5% interest per month.",
|
|
222
|
+
"The Customer shall pay the annual fees in advance."],
|
|
223
|
+
"intellectual property": ["All intellectual property created under this Agreement vests in the Customer.",
|
|
224
|
+
"The Supplier retains all rights, title and interest in its pre-existing software."],
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def clauses(rng, i):
|
|
229
|
+
t = pick(rng, list(CLAUSES))
|
|
230
|
+
text = "%d.%d %s" % (rng.randint(2, 18), rng.randint(1, 9), pick(rng, CLAUSES[t]).format(n=pick(rng, [12, 30, 45, 60, 90])))
|
|
231
|
+
return {"id": i, "contract": "MSA-%04d" % rng.randint(1, 9999), "text": text, "clause_type": t}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ------------------------------------------------------------------ compliance emails
|
|
235
|
+
E_BAD = ["This fund is guaranteed to return {n}% a year, zero risk.", "I promise you will double your money by {m}.",
|
|
236
|
+
"Our clients never lose: a guaranteed {n}% return.", "Risk-free investment with assured profits of {n}%."]
|
|
237
|
+
E_OK = ["Past performance does not guarantee future results.", "The fund returned {n}% last year; returns may vary.",
|
|
238
|
+
"Attached is the quarterly statement for your review.", "Let's meet on {m} to review your risk profile.",
|
|
239
|
+
"Markets were volatile this quarter; your portfolio is diversified."]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def emails(rng, i):
|
|
243
|
+
bad = rng.random() < .25
|
|
244
|
+
body = "Dear %s, %s %s Best, %s" % (pick(rng, FIRST), pick(rng, E_BAD if bad else E_OK).format(
|
|
245
|
+
n=rng.randint(5, 40), m=pick(rng, MONTHS)), pick(rng, ["", "Call me anytime.", "Happy to discuss."]),
|
|
246
|
+
pick(rng, FIRST))
|
|
247
|
+
return {"id": i, "subject": pick(rng, ["Your portfolio", "Opportunity", "Quarterly update", "Follow-up"]),
|
|
248
|
+
"body": body, "violation": bad}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ------------------------------------------------------------------ entity resolution: company pairs
|
|
252
|
+
CO_A = ["Acme", "Globex", "Initech", "Umbrella", "Stark", "Wayne", "Hooli", "Vandelay", "Wonka", "Tyrell", "Cyberdyne",
|
|
253
|
+
"Soylent", "Massive Dynamic", "Pied Piper", "Aperture"]
|
|
254
|
+
CO_B = ["Logistics", "Labs", "Industries", "Foods", "Energy", "Analytics", "Health", "Robotics", "Capital", "Media"]
|
|
255
|
+
SUFFIX = [("Inc.", "Incorporated"), ("LLC", "L.L.C."), ("Ltd", "Limited"), ("Corp.", "Corporation"), ("GmbH", "GmbH")]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def company_pairs(rng, i):
|
|
259
|
+
a, b = pick(rng, CO_A), pick(rng, CO_B)
|
|
260
|
+
s1, s2 = pick(rng, SUFFIX)
|
|
261
|
+
city = pick(rng, CITIES)
|
|
262
|
+
left = "%s %s %s, %s" % (a, b, s1, city)
|
|
263
|
+
same = rng.random() < .5
|
|
264
|
+
if same:
|
|
265
|
+
variants = ["%s %s %s, %s" % (a.upper(), b.upper(), s2, city), "The %s %s Company (%s)" % (a, b, city),
|
|
266
|
+
"%s-%s %s, %s" % (a, b, s1, city), "%s %s, %s office" % (a, b, city)]
|
|
267
|
+
right = pick(rng, variants)
|
|
268
|
+
else:
|
|
269
|
+
other = pick(rng, [x for x in CO_B if x != b]) if rng.random() < .6 else b
|
|
270
|
+
a2 = a if other != b else pick(rng, [x for x in CO_A if x != a])
|
|
271
|
+
right = "%s %s %s, %s" % (a2, other, pick(rng, SUFFIX)[0], pick(rng, CITIES))
|
|
272
|
+
return {"id": i, "record_a": left, "record_b": right, "same": same}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
TABLES = {"tickets": tickets, "reviews": reviews, "adverse_events": adverse_events, "transactions": transactions,
|
|
276
|
+
"job_posts": job_posts, "listings": listings, "clauses": clauses, "emails": emails,
|
|
277
|
+
"company_pairs": company_pairs}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def generate(rows_per_table, seed=11, dup_rate=0.03):
|
|
281
|
+
out = {}
|
|
282
|
+
for t, fn in TABLES.items():
|
|
283
|
+
rng = random.Random("%s:%s" % (seed, t))
|
|
284
|
+
n = rows_per_table * (2 if t == "company_pairs" else 1) # 9 tables; pairs get double -> 10 x rows
|
|
285
|
+
rows = []
|
|
286
|
+
for i in range(1, n + 1):
|
|
287
|
+
rows.append({**rng.choice(rows), "id": i} if rows and rng.random() < dup_rate else fn(rng, i))
|
|
288
|
+
out[t] = rows
|
|
289
|
+
return out
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
p = argparse.ArgumentParser()
|
|
294
|
+
p.add_argument("--rows-per-table", type=int, default=10_000)
|
|
295
|
+
p.add_argument("--seed", type=int, default=11)
|
|
296
|
+
p.add_argument("--out", default="bench/bench.duckdb")
|
|
297
|
+
a = p.parse_args()
|
|
298
|
+
import duckdb
|
|
299
|
+
import pyarrow as pa
|
|
300
|
+
con = duckdb.connect(a.out)
|
|
301
|
+
total = 0
|
|
302
|
+
for t, rows in generate(a.rows_per_table, a.seed).items():
|
|
303
|
+
tbl = pa.Table.from_pylist(rows) # noqa: F841 -- referenced by name in SQL
|
|
304
|
+
con.execute("CREATE OR REPLACE TABLE %s AS SELECT * FROM tbl" % t)
|
|
305
|
+
total += len(rows)
|
|
306
|
+
print(" %-15s %6d rows" % (t, len(rows)))
|
|
307
|
+
print("wrote %d rows, %d tasks, to %s" % (total, len(TASKS), a.out))
|
sqljev/duckdb.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""DuckDB: register the jev functions on a connection as vectorized (Arrow) UDFs.
|
|
2
|
+
|
|
3
|
+
import duckdb, sqljev.duckdb
|
|
4
|
+
con = duckdb.connect()
|
|
5
|
+
sqljev.duckdb.register(con) # SQLJEV_* settings, or register(con, backend="gateway")
|
|
6
|
+
con.sql("SELECT * FROM tickets t WHERE jev(to_json(t), 'the customer is angry')")
|
|
7
|
+
|
|
8
|
+
DuckDB hands each UDF a whole vector (2,048 rows), so every call is one engine.call() over the chunk.
|
|
9
|
+
"""
|
|
10
|
+
from .core import Jev
|
|
11
|
+
|
|
12
|
+
_SIGNATURES = {
|
|
13
|
+
# name: (parameter types, return type)
|
|
14
|
+
"jev": (["VARCHAR", "VARCHAR"], "BOOLEAN"),
|
|
15
|
+
"jev_prob": (["VARCHAR", "VARCHAR"], "DOUBLE"),
|
|
16
|
+
"jev_score": (["VARCHAR", "VARCHAR", "VARCHAR[]"], "DOUBLE"),
|
|
17
|
+
"jev_score_norm": (["VARCHAR", "VARCHAR", "VARCHAR[]"], "DOUBLE"),
|
|
18
|
+
"jev_choice": (["VARCHAR", "VARCHAR", "VARCHAR[]"], "VARCHAR"),
|
|
19
|
+
"jev_confidence": (["VARCHAR", "VARCHAR", "VARCHAR", "VARCHAR[]"], "DOUBLE"),
|
|
20
|
+
"jev_eval": (["VARCHAR", "VARCHAR", "VARCHAR", "VARCHAR[]"], "VARCHAR"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(con, engine=None, prefix="", **settings):
|
|
25
|
+
"""Create jev, jev_prob, jev_score, jev_score_norm, jev_choice, jev_confidence, jev_eval on `con`.
|
|
26
|
+
Returns the engine, whose .stats() reports requests, cache hits and forward passes."""
|
|
27
|
+
import json
|
|
28
|
+
import pyarrow as pa
|
|
29
|
+
from duckdb import sqltype
|
|
30
|
+
|
|
31
|
+
eng = engine or Jev(**settings)
|
|
32
|
+
|
|
33
|
+
def make(name, ret, arity):
|
|
34
|
+
def run(*cols):
|
|
35
|
+
calls = list(zip(*(c.to_pylist() for c in cols)))
|
|
36
|
+
out = eng.call(name, calls)
|
|
37
|
+
if name == "jev_eval":
|
|
38
|
+
out = [None if v is None else json.dumps(v) for v in out]
|
|
39
|
+
return pa.array(out, type=ret)
|
|
40
|
+
# DuckDB checks the Python signature, so each UDF needs a fixed arity.
|
|
41
|
+
if arity == 2:
|
|
42
|
+
return lambda a, b: run(a, b)
|
|
43
|
+
if arity == 3:
|
|
44
|
+
return lambda a, b, c: run(a, b, c)
|
|
45
|
+
return lambda a, b, c, d: run(a, b, c, d)
|
|
46
|
+
|
|
47
|
+
arrow_types = {"BOOLEAN": pa.bool_(), "DOUBLE": pa.float64(), "VARCHAR": pa.string()}
|
|
48
|
+
for name, (params, ret) in _SIGNATURES.items():
|
|
49
|
+
full = prefix + name
|
|
50
|
+
try:
|
|
51
|
+
con.remove_function(full)
|
|
52
|
+
except Exception: # noqa: BLE001 -- not registered yet
|
|
53
|
+
pass
|
|
54
|
+
con.create_function(full, make(name, arrow_types[ret], len(params)), [sqltype(p) for p in params], sqltype(ret),
|
|
55
|
+
type="arrow", null_handling="special", side_effects=False)
|
|
56
|
+
return eng
|