db_setup.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. from sqlite3 import OperationalError
  2. from game import CURRENCY_NAME, MINIMUM_ORDER_AMOUNT
  3. def setup(cursor):
  4. print('Database setup...')
  5. drop_triggers(cursor)
  6. tables(cursor)
  7. create_triggers(cursor)
  8. create_indices(cursor)
  9. seed(cursor)
  10. def drop_triggers(cursor):
  11. print(' - Dropping all triggers...')
  12. cursor.execute("DROP TRIGGER IF EXISTS owned_amount_not_negative_after_insert")
  13. cursor.execute("DROP TRIGGER IF EXISTS owned_amount_not_negative_after_update")
  14. cursor.execute("DROP TRIGGER IF EXISTS amount_positive_after_insert")
  15. cursor.execute("DROP TRIGGER IF EXISTS amount_positive_after_update")
  16. cursor.execute("DROP TRIGGER IF EXISTS order_limit_not_negative_after_insert")
  17. cursor.execute("DROP TRIGGER IF EXISTS order_limit_not_negative_after_update")
  18. cursor.execute("DROP TRIGGER IF EXISTS order_amount_positive_after_insert")
  19. cursor.execute("DROP TRIGGER IF EXISTS order_amount_positive_after_update")
  20. cursor.execute("DROP TRIGGER IF EXISTS not_more_executed_than_ordered_after_insert")
  21. cursor.execute("DROP TRIGGER IF EXISTS not_more_executed_than_ordered_after_update")
  22. cursor.execute("DROP TRIGGER IF EXISTS not_more_ordered_than_available_after_insert")
  23. cursor.execute("DROP TRIGGER IF EXISTS not_more_ordered_than_available_after_update")
  24. cursor.execute("DROP TRIGGER IF EXISTS expiry_dt_in_future_after_insert")
  25. cursor.execute("DROP TRIGGER IF EXISTS expiry_dt_in_future_after_update")
  26. cursor.execute("DROP TRIGGER IF EXISTS stop_loss_requires_limit_after_insert")
  27. cursor.execute("DROP TRIGGER IF EXISTS stop_loss_requires_limit_after_update")
  28. cursor.execute("DROP TRIGGER IF EXISTS limit_requires_stop_loss_after_insert")
  29. cursor.execute("DROP TRIGGER IF EXISTS limit_requires_stop_loss_after_update")
  30. cursor.execute("DROP TRIGGER IF EXISTS minimum_order_amount_after_insert")
  31. cursor.execute("DROP TRIGGER IF EXISTS integer_amount_after_insert")
  32. cursor.execute("DROP TRIGGER IF EXISTS dt_monotonic_after_insert")
  33. cursor.execute("DROP TRIGGER IF EXISTS dt_monotonic_after_update")
  34. cursor.execute("DROP TRIGGER IF EXISTS orders_rowid_sorted_by_creation_time_after_insert")
  35. cursor.execute("DROP TRIGGER IF EXISTS news_dt_monotonic_after_update")
  36. cursor.execute("DROP TRIGGER IF EXISTS not_nullify_buyer_id_after_update")
  37. cursor.execute("DROP TRIGGER IF EXISTS buyer_id_not_null_after_insert")
  38. cursor.execute("DROP TRIGGER IF EXISTS not_nullify_seller_id_after_update")
  39. cursor.execute("DROP TRIGGER IF EXISTS seller_id_not_null_after_insert")
  40. cursor.execute("DROP TRIGGER IF EXISTS order_history_no_update")
  41. def seed(cursor):
  42. print(' - Seeding initial data...')
  43. # ₭ollar
  44. cursor.execute('''
  45. INSERT OR IGNORE INTO ownables
  46. (name)
  47. VALUES (?)
  48. ''', (CURRENCY_NAME,))
  49. # The bank/external investors
  50. cursor.execute('''
  51. INSERT OR IGNORE INTO users
  52. (username,password)
  53. VALUES ('bank','')
  54. ''')
  55. # bank owns all the money that is not owned by players, 1000 * num_used_key - player_money
  56. cursor.execute('''
  57. INSERT OR IGNORE INTO ownership
  58. (user_id, ownable_id, amount)
  59. VALUES ((SELECT rowid FROM users WHERE username = 'bank'),
  60. (SELECT rowid FROM ownables WHERE name = ?),
  61. 1000 * (SELECT COUNT(used_by_user_id) FROM keys) - (SELECT SUM(amount)
  62. FROM ownership
  63. WHERE ownable_id = (SELECT rowid FROM ownables WHERE name = ?)))
  64. ''', (CURRENCY_NAME, CURRENCY_NAME,))
  65. # bank owns some stuff (₭ollar is be dealt with separately)
  66. cursor.execute('''
  67. INSERT OR IGNORE INTO ownership
  68. (user_id, ownable_id, amount)
  69. SELECT (SELECT rowid FROM users WHERE username = 'bank'),
  70. ownables.rowid,
  71. (SELECT COALESCE(SUM(amount),0) FROM ownership WHERE ownable_id = ownables.rowid)
  72. FROM ownables WHERE
  73. name <> ?
  74. ''', (CURRENCY_NAME,))
  75. def create_triggers(cursor):
  76. print(' - Creating triggers...')
  77. cursor.execute('''
  78. CREATE TRIGGER IF NOT EXISTS owned_amount_not_negative_after_insert
  79. AFTER INSERT ON ownership
  80. WHEN NEW.amount < 0
  81. BEGIN SELECT RAISE(ROLLBACK, 'Can not own an amount less than 0.'); END
  82. ''')
  83. cursor.execute('''
  84. CREATE TRIGGER IF NOT EXISTS owned_amount_not_negative_after_update
  85. AFTER UPDATE ON ownership
  86. WHEN NEW.amount < 0
  87. BEGIN SELECT RAISE(ROLLBACK, 'Can not own an amount less than 0.'); END
  88. ''')
  89. cursor.execute('''
  90. CREATE TRIGGER IF NOT EXISTS amount_positive_after_insert
  91. AFTER INSERT ON transactions
  92. WHEN NEW.amount <= 0
  93. BEGIN SELECT RAISE(ROLLBACK, 'Can not perform empty transactions.'); END
  94. ''')
  95. cursor.execute('''
  96. CREATE TRIGGER IF NOT EXISTS amount_positive_after_update
  97. AFTER UPDATE ON transactions
  98. WHEN NEW.amount <= 0
  99. BEGIN SELECT RAISE(ROLLBACK, 'Can not perform empty transactions.'); END
  100. ''')
  101. cursor.execute('''
  102. CREATE TRIGGER IF NOT EXISTS order_limit_not_negative_after_insert
  103. AFTER INSERT ON orders
  104. WHEN NEW."limit" IS NOT NULL AND NEW."limit" < 0
  105. BEGIN SELECT RAISE(ROLLBACK, 'Can not set a limit less than 0.'); END
  106. ''')
  107. cursor.execute('''
  108. CREATE TRIGGER IF NOT EXISTS order_limit_not_negative_after_update
  109. AFTER UPDATE ON orders
  110. WHEN NEW."limit" IS NOT NULL AND NEW."limit" < 0
  111. BEGIN SELECT RAISE(ROLLBACK, 'Can not set a limit less than 0.'); END
  112. ''')
  113. cursor.execute('''
  114. CREATE TRIGGER IF NOT EXISTS order_amount_positive_after_insert
  115. AFTER INSERT ON orders
  116. WHEN NEW.ordered_amount <= 0 OR NEW.executed_amount < 0
  117. BEGIN SELECT RAISE(ROLLBACK, 'Can not order 0 or less.'); END
  118. ''')
  119. cursor.execute('''
  120. CREATE TRIGGER IF NOT EXISTS order_amount_positive_after_update
  121. AFTER UPDATE ON orders
  122. WHEN NEW.ordered_amount <= 0 OR NEW.executed_amount < 0
  123. BEGIN SELECT RAISE(ROLLBACK, 'Can not order 0 or less.'); END
  124. ''')
  125. cursor.execute('''
  126. CREATE TRIGGER IF NOT EXISTS not_more_executed_than_ordered_after_insert
  127. AFTER INSERT ON orders
  128. WHEN NEW.ordered_amount < NEW.executed_amount
  129. BEGIN SELECT RAISE(ROLLBACK, 'Can not execute more than ordered.'); END
  130. ''')
  131. cursor.execute('''
  132. CREATE TRIGGER IF NOT EXISTS not_more_executed_than_ordered_after_update
  133. AFTER UPDATE ON orders
  134. WHEN NEW.ordered_amount < NEW.executed_amount
  135. BEGIN SELECT RAISE(ROLLBACK, 'Can not execute more than ordered.'); END
  136. ''')
  137. cursor.execute('''
  138. CREATE TRIGGER IF NOT EXISTS not_more_ordered_than_available_after_insert
  139. AFTER INSERT ON orders
  140. WHEN NOT NEW.buy AND 0 >
  141. -- owned_amount
  142. COALESCE (
  143. (SELECT amount
  144. FROM ownership
  145. WHERE ownership.rowid = NEW.ownership_id), 0)
  146. - -- sell_ordered_amount
  147. (SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
  148. FROM orders, ownership
  149. WHERE ownership.rowid = orders.ownership_id
  150. AND ownership.rowid = NEW.ownership_id
  151. AND NOT orders.buy)
  152. BEGIN SELECT RAISE(ROLLBACK, 'Can not order more than you own.'); END
  153. ''')
  154. cursor.execute('''
  155. CREATE TRIGGER IF NOT EXISTS not_more_ordered_than_available_after_update
  156. AFTER UPDATE ON orders
  157. WHEN NOT NEW.buy AND 0 >
  158. -- owned_amount
  159. COALESCE (
  160. (SELECT amount
  161. FROM ownership
  162. WHERE ownership.rowid = NEW.ownership_id), 0)
  163. - -- sell_ordered_amount
  164. (SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
  165. FROM orders, ownership
  166. WHERE ownership.rowid = orders.ownership_id
  167. AND ownership.rowid = NEW.ownership_id
  168. AND NOT orders.buy)
  169. BEGIN SELECT RAISE(ROLLBACK, 'Can not order more than you own.'); END
  170. ''')
  171. cursor.execute('''
  172. CREATE TRIGGER IF NOT EXISTS expiry_dt_in_future_after_insert
  173. AFTER INSERT ON orders
  174. WHEN NEW.expiry_dt <= datetime('now')
  175. BEGIN SELECT RAISE(ROLLBACK, 'Order is already expired.'); END
  176. ''')
  177. cursor.execute('''
  178. CREATE TRIGGER IF NOT EXISTS expiry_dt_in_future_after_update
  179. AFTER UPDATE ON orders
  180. WHEN NEW.expiry_dt <= datetime('now')
  181. BEGIN SELECT RAISE(ROLLBACK, 'Order is already expired.'); END
  182. ''')
  183. cursor.execute('''
  184. CREATE TRIGGER IF NOT EXISTS stop_loss_requires_limit_after_insert
  185. AFTER INSERT ON orders
  186. WHEN NEW."limit" IS NULL AND NEW.stop_loss IS NOT NULL
  187. BEGIN SELECT RAISE(ROLLBACK, 'Can only set `stop_loss` `for limit orders.'); END
  188. ''')
  189. cursor.execute('''
  190. CREATE TRIGGER IF NOT EXISTS stop_loss_requires_limit_after_update
  191. AFTER UPDATE ON orders
  192. WHEN NEW."limit" IS NULL AND NEW.stop_loss IS NOT NULL
  193. BEGIN SELECT RAISE(ROLLBACK, 'Can only set `stop_loss` `for limit orders.'); END
  194. ''')
  195. cursor.execute('''
  196. CREATE TRIGGER IF NOT EXISTS limit_requires_stop_loss_after_insert
  197. AFTER INSERT ON orders
  198. WHEN NEW."limit" IS NOT NULL AND NEW.stop_loss IS NULL
  199. BEGIN SELECT RAISE(ROLLBACK, 'Need to set stop_loss to either True or False for limit orders.'); END
  200. ''')
  201. cursor.execute('''
  202. CREATE TRIGGER IF NOT EXISTS limit_requires_stop_loss_after_update
  203. AFTER UPDATE ON orders
  204. WHEN NEW."limit" IS NOT NULL AND NEW.stop_loss IS NULL
  205. BEGIN SELECT RAISE(ROLLBACK, 'Need to set stop_loss to either True or False for limit orders.'); END
  206. ''')
  207. cursor.execute('''
  208. CREATE TRIGGER IF NOT EXISTS minimum_order_amount_after_insert
  209. AFTER INSERT ON orders
  210. WHEN NEW.ordered_amount < ?
  211. BEGIN SELECT RAISE(ROLLBACK, 'There is a minimum amount for new orders.'); END
  212. '''.replace('?', str(MINIMUM_ORDER_AMOUNT)))
  213. cursor.execute('''
  214. CREATE TRIGGER IF NOT EXISTS integer_amount_after_insert
  215. AFTER INSERT ON orders
  216. WHEN NEW.ordered_amount <> ROUND(NEW.ordered_amount)
  217. BEGIN SELECT RAISE(ROLLBACK, 'Can only set integer amounts for new orders.'); END
  218. '''.replace('?', str(MINIMUM_ORDER_AMOUNT)))
  219. cursor.execute('''
  220. CREATE TRIGGER IF NOT EXISTS dt_monotonic_after_insert
  221. AFTER INSERT ON transactions
  222. WHEN NEW.dt < (SELECT MAX(dt) FROM transactions t2 WHERE t2.rowid < rowid)
  223. BEGIN SELECT RAISE(ROLLBACK, 'Transaction rowid programming bug, not your fault.'); END
  224. ''')
  225. cursor.execute('''
  226. CREATE TRIGGER IF NOT EXISTS dt_monotonic_after_update
  227. AFTER INSERT ON transactions
  228. WHEN NEW.dt < (SELECT MAX(dt) FROM transactions t2 WHERE t2.rowid < rowid)
  229. BEGIN SELECT RAISE(ROLLBACK, 'Transaction rowid programming bug, not your fault.'); END
  230. ''')
  231. cursor.execute('''
  232. CREATE TRIGGER IF NOT EXISTS news_dt_monotonic_after_insert
  233. AFTER INSERT ON news
  234. WHEN NEW.dt < (SELECT MAX(dt) FROM news t2 WHERE t2.rowid < rowid)
  235. BEGIN SELECT RAISE(ROLLBACK, 'Transaction rowid programming bug, not your fault.'); END
  236. ''')
  237. cursor.execute('''
  238. CREATE TRIGGER IF NOT EXISTS news_dt_monotonic_after_insert
  239. AFTER INSERT ON news
  240. WHEN NEW.dt < (SELECT MAX(dt) FROM news t2 WHERE t2.rowid < rowid)
  241. BEGIN SELECT RAISE(ROLLBACK, 'Transaction rowid programming bug, not your fault.'); END
  242. ''')
  243. cursor.execute('''
  244. CREATE TRIGGER IF NOT EXISTS orders_rowid_sorted_by_creation_time_after_insert
  245. AFTER INSERT ON orders
  246. WHEN NEW.rowid < (SELECT MAX(rowid) FROM orders o2)
  247. BEGIN SELECT RAISE(ROLLBACK, 'Order-rowid programming bug (insert), not your fault.'); END
  248. ''')
  249. cursor.execute('''
  250. CREATE TRIGGER IF NOT EXISTS news_dt_monotonic_after_update
  251. AFTER UPDATE ON orders
  252. WHEN NEW.rowid <> OLD.rowid
  253. BEGIN SELECT RAISE(ROLLBACK, 'Cannot change number of existing order.'); END
  254. ''')
  255. cursor.execute('''
  256. CREATE TRIGGER IF NOT EXISTS not_nullify_buyer_id_after_update
  257. AFTER UPDATE ON transactions
  258. WHEN NEW.buyer_id IS NULL AND OLD.buyer_id IS NOT NULL
  259. BEGIN SELECT RAISE(ROLLBACK, 'Cannot nullify buyer_id.'); END
  260. ''')
  261. cursor.execute('''
  262. CREATE TRIGGER IF NOT EXISTS buyer_id_not_null_after_insert
  263. AFTER INSERT ON transactions
  264. WHEN NEW.buyer_id IS NULL
  265. BEGIN SELECT RAISE(ROLLBACK, 'buyer_id must not be null for new transactions.'); END
  266. ''')
  267. cursor.execute('''
  268. CREATE TRIGGER IF NOT EXISTS not_nullify_seller_id_after_update
  269. AFTER UPDATE ON transactions
  270. WHEN NEW.seller_id IS NULL AND OLD.seller_id IS NOT NULL
  271. BEGIN SELECT RAISE(ROLLBACK, 'Cannot nullify seller_id.'); END
  272. ''')
  273. cursor.execute('''
  274. CREATE TRIGGER IF NOT EXISTS seller_id_not_null_after_insert
  275. AFTER INSERT ON transactions
  276. WHEN NEW.seller_id IS NULL
  277. BEGIN SELECT RAISE(ROLLBACK, 'seller_id must not be null for new transactions.'); END
  278. ''')
  279. cursor.execute('''
  280. CREATE TRIGGER IF NOT EXISTS order_history_no_update
  281. BEFORE UPDATE ON order_history
  282. BEGIN SELECT RAISE(ROLLBACK, 'Can not change order history.'); END
  283. ''')
  284. def create_indices(cursor):
  285. print(' - Creating indices...')
  286. cursor.execute('''
  287. CREATE INDEX IF NOT EXISTS ownership_ownable
  288. ON ownership (ownable_id)
  289. ''')
  290. cursor.execute('''
  291. CREATE INDEX IF NOT EXISTS transactions_ownable
  292. ON transactions (ownable_id)
  293. ''')
  294. cursor.execute('''
  295. CREATE INDEX IF NOT EXISTS orders_expiry
  296. ON orders (expiry_dt)
  297. ''')
  298. cursor.execute('''
  299. CREATE INDEX IF NOT EXISTS orders_ownership
  300. ON orders (ownership_id)
  301. ''')
  302. cursor.execute('''
  303. CREATE INDEX IF NOT EXISTS orders_limit
  304. ON orders ("limit")
  305. ''')
  306. cursor.execute('''
  307. CREATE INDEX IF NOT EXISTS transactions_dt
  308. ON transactions (dt)
  309. ''')
  310. cursor.execute('''
  311. CREATE INDEX IF NOT EXISTS news_dt
  312. ON news (dt)
  313. ''')
  314. cursor.execute('''
  315. CREATE INDEX IF NOT EXISTS ownables_name
  316. ON ownables (name)
  317. ''')
  318. cursor.execute('''
  319. CREATE INDEX IF NOT EXISTS users_name
  320. ON users (username)
  321. ''')
  322. cursor.execute('''
  323. CREATE INDEX IF NOT EXISTS sessions_id
  324. ON sessions (session_id)
  325. ''')
  326. cursor.execute('''
  327. CREATE INDEX IF NOT EXISTS sessions_user
  328. ON sessions (user_id)
  329. ''')
  330. cursor.execute('''
  331. CREATE INDEX IF NOT EXISTS transactions_seller
  332. ON transactions (seller_id)
  333. ''')
  334. cursor.execute('''
  335. CREATE INDEX IF NOT EXISTS transactions_buyer
  336. ON transactions (buyer_id)
  337. ''')
  338. cursor.execute('''
  339. CREATE INDEX IF NOT EXISTS order_history_id
  340. ON order_history (order_id)
  341. ''')
  342. cursor.execute('''
  343. CREATE INDEX IF NOT EXISTS order_canceled
  344. ON order_history (archived_dt)
  345. ''')
  346. cursor.execute('''
  347. CREATE INDEX IF NOT EXISTS order_history_ownership
  348. ON order_history (ownership_id)
  349. ''')
  350. def tables(cursor):
  351. print(' - Creating tables...')
  352. cursor.execute('''
  353. CREATE TABLE IF NOT EXISTS users(
  354. username VARCHAR(10) UNIQUE NOT NULL,
  355. password VARCHAR(200) NOT NULL)
  356. ''')
  357. cursor.execute('''
  358. CREATE TABLE IF NOT EXISTS ownables(
  359. name VARCHAR(10) UNIQUE NOT NULL)
  360. ''')
  361. cursor.execute('''
  362. CREATE TABLE IF NOT EXISTS ownership(
  363. user_id INTEGER NOT NULL,
  364. ownable_id INTEGER NOT NULL,
  365. amount CURRENCY NOT NULL DEFAULT 0,
  366. FOREIGN KEY (user_id) REFERENCES users(rowid),
  367. FOREIGN KEY (ownable_id) REFERENCES ownables(rowid),
  368. UNIQUE (user_id, ownable_id)
  369. )
  370. ''')
  371. cursor.execute('''
  372. CREATE TABLE IF NOT EXISTS sessions(
  373. user_id INTEGER NOT NULL,
  374. session_id STRING NOT NULL,
  375. FOREIGN KEY (user_id) REFERENCES users(rowid)
  376. )
  377. ''')
  378. cursor.execute('''
  379. CREATE TABLE IF NOT EXISTS orders(
  380. ownership_id INTEGER NOT NULL,
  381. buy BOOLEAN NOT NULL,
  382. "limit" CURRENCY,
  383. stop_loss BOOLEAN,
  384. ordered_amount CURRENCY NOT NULL,
  385. executed_amount CURRENCY DEFAULT 0 NOT NULL,
  386. expiry_dt DATETIME NOT NULL,
  387. FOREIGN KEY (ownership_id) REFERENCES ownership(rowid)
  388. )
  389. ''')
  390. cursor.execute('''
  391. CREATE TABLE IF NOT EXISTS order_history(
  392. ownership_id INTEGER NOT NULL,
  393. buy BOOLEAN NOT NULL,
  394. "limit" CURRENCY,
  395. ordered_amount CURRENCY NOT NULL,
  396. executed_amount CURRENCY NOT NULL,
  397. expiry_dt DATETIME NOT NULL,
  398. status VARCHAR(20) NOT NULL,
  399. order_id INTEGER NOT NULL,
  400. archived_dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  401. FOREIGN KEY (ownership_id) REFERENCES ownership(rowid)
  402. -- order_id is not a FOREIGN KEY since orders are deleted from order table afterwards
  403. )
  404. ''')
  405. cursor.execute('''
  406. CREATE TABLE IF NOT EXISTS transactions(
  407. dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  408. price CURRENCY NOT NULL,
  409. ownable_id INTEGER NOT NULL,
  410. amount CURRENCY NOT NULL,
  411. FOREIGN KEY (ownable_id) REFERENCES ownable(rowid)
  412. )
  413. ''')
  414. _add_column_if_not_exists(cursor, '''
  415. -- there is a not null constraint for new values that is watched by triggers
  416. ALTER TABLE transactions ADD COLUMN buyer_id INTEGER REFERENCES user(rowid)
  417. ''')
  418. _add_column_if_not_exists(cursor, '''
  419. -- there is a not null constraint for new values that is watched by triggers
  420. ALTER TABLE transactions ADD COLUMN seller_id INTEGER REFERENCES user(rowid)
  421. ''')
  422. cursor.execute('''
  423. CREATE TABLE IF NOT EXISTS keys(
  424. key STRING UNIQUE NOT NULL,
  425. used_by_user_id INTEGER,
  426. FOREIGN KEY (used_by_user_id) REFERENCES user(rowid)
  427. )
  428. ''')
  429. cursor.execute('''
  430. CREATE TABLE IF NOT EXISTS news(
  431. dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  432. title VARCHAR(50) NOT NULL
  433. )
  434. ''')
  435. def _add_column_if_not_exists(cursor, query):
  436. if 'ALTER TABLE' not in query:
  437. raise ValueError('Only alter table queries allowed.')
  438. if 'ADD COLUMN' not in query:
  439. raise ValueError('Only add column queries allowed.')
  440. try:
  441. cursor.execute(query)
  442. except OperationalError as e: # if the column already exists this will happen
  443. if 'duplicate column name' not in e.args[0]:
  444. raise