db_setup.py 18 KB

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