forked from probcomp/bayeslite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
HACKING
90 lines (68 loc) · 3.42 KB
/
HACKING
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
Guidelines for writing bayeslite software
This working document contains guidelines for how to develop against
the bayeslite API.
* SQL/BQL parameters
Use SQL/BQL parameters to pass strings and other values into SQL/BQL.
DO NOT use format strings.
DO: cursor.execute('UPDATE foo SET x = ? WHERE id = ?', (x, id))
DON'T: cursor.execute("UPDATE foo SET x = '%s' WHERE id = %d" % (x, id))
DON'T: cursor.execute("UPDATE foo SET x = '{}' WHERE id = {}".format(x, id))
DO: cursor.execute('SELECT x, y FROM t WHERE z = ?', (z,))
DON'T: cursor.execute('SELECT x, y FROM t WHERE z = ?', z)
DON'T: cursor.execute('SELECT x, y FROM t WHERE z = {}'.format(z))
Prefer named parameters if the query has more than one parameter and
covers multiple lines:
cursor = db.cursor().execute('''
SELECT COUNT(*)
FROM bayesdb_generator AS g, bayesdb_column AS c
WHERE g.id = :generator_id
AND g.tabname = c.tabname
AND c.colno = :colno
''', {
'generator_id': generator_id,
'colno': colno,
})
If the tables and columns in the query are determined dynamically,
then use bql_quote_name and format strings to assemble SQL/BQL
queries. But prefer to avoid this by writing different queries or
reusing subroutines that already do it, such as in bayeslite.core.
DO: from bayeslite import bql_quote_name
qt = bql_quote_name(table)
qc = bql_quote_name(column)
cursor.execute('SELECT %s FROM %s WHERE x = ?' % (qc, qt), (x,))
DON'T: cursor.execute('SELECT %s FROM %s WHERE x = ?' % (column, table), (x,))
DON'T: cursor.execute('SELECT %s FROM %s WHERE x = %d' % (qc, qt, x))
* SQL updates
When issuing an UPDATE command to sqlite3, if you can count the number
of rows it should affect, do so and assert that it affected that many
rows:
total_changes = bdb._sqlite3.totalchanges()
bdb.sql_execute('UPDATE ...', (...))
assert bdb._sqlite3.totalchanges() - total_changes == 1
* Randomization
Avoid indiscriminate nondeterminism.
All random choices should be made from PRNGs with seeds that the user
can control, via the normal Python API and the bayeslite shell. Any
actual nondeterminism should be clearly labelled as such, e.g. a
future shell command to choose a seed from /dev/urandom.
To write nondeterministic tests that explore an intentionally
unpredictable source of inputs, instead of testing exactly the same
input every time, write a deterministic function of a 32-byte seed and
use the @stochastic decorator to vary it:
from stochastic import stochastic
@stochastic(max_runs=4, min_passes=2)
def test_quagga(seed):
frobnicate(seed)
This defines test_quagga to be a function that accepts an *optional*
seed argument. If you call it with zero arguments, then it will call
frobnicate up to four times, and if it does not pass twice, it will
raise a StochasticError that includes (a) the last exception with
which frobnicate failed and (b) the last seed with which frobnicate
failed.
You can then retry using exactly the same seed by calling test_quagga
manually with the seed as its argument:
>>> test_quagga()
StochasticError: [seed 434529bf3e5a16930701b55c39a90acfcd115ba0cada99f5af5448f3b96923dd]
ZigException: something set us up the bomb
>>> test_quagga('434529bf3e5a16930701b55c39a90acfcd115ba0cada99f5af5448f3b96923dd'.decode('hex'))
ZigException: something set us up the bomb