@actual-app/core 26.3.0

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.
Files changed (358) hide show
  1. package/.swcrc +11 -0
  2. package/bin/build-browser +40 -0
  3. package/bin/copy-migrations +9 -0
  4. package/db.sqlite +0 -0
  5. package/default-db.sqlite +0 -0
  6. package/migrations/.force-copy-windows +0 -0
  7. package/migrations/1548957970627_remove-db-version.sql +5 -0
  8. package/migrations/1550601598648_payees.sql +23 -0
  9. package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
  10. package/migrations/1561751833510_indexes.sql +7 -0
  11. package/migrations/1567699552727_budget.sql +38 -0
  12. package/migrations/1582384163573_cleared.sql +6 -0
  13. package/migrations/1597756566448_rules.sql +10 -0
  14. package/migrations/1608652596043_parent_field.sql +13 -0
  15. package/migrations/1608652596044_trans_views.sql +56 -0
  16. package/migrations/1612625548236_optimize.sql +7 -0
  17. package/migrations/1614782639336_trans_views2.sql +33 -0
  18. package/migrations/1615745967948_meta.sql +10 -0
  19. package/migrations/1616167010796_accounts_order.sql +5 -0
  20. package/migrations/1618975177358_schedules.sql +28 -0
  21. package/migrations/1632571489012_remove_cache.js +136 -0
  22. package/migrations/1679728867040_rules_conditions.sql +5 -0
  23. package/migrations/1681115033845_add_schedule_name.sql +5 -0
  24. package/migrations/1682974838138_remove_payee_rules.sql +5 -0
  25. package/migrations/1685007876842_add_category_hidden.sql +6 -0
  26. package/migrations/1686139660866_remove_account_type.sql +5 -0
  27. package/migrations/1688749527273_transaction_filters.sql +10 -0
  28. package/migrations/1688841238000_add_account_type.sql +5 -0
  29. package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
  30. package/migrations/1694438752000_add_goal_targets.sql +7 -0
  31. package/migrations/1697046240000_add_reconciled.sql +5 -0
  32. package/migrations/1704572023730_add_account_sync_source.sql +5 -0
  33. package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
  34. package/migrations/1707267033000_reports.sql +28 -0
  35. package/migrations/1712784523000_unhide_input_group.sql +8 -0
  36. package/migrations/1716359441000_include_current.sql +5 -0
  37. package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
  38. package/migrations/1720664867241_add_payee_favorite.sql +5 -0
  39. package/migrations/1720665000000_goal_context.sql +6 -0
  40. package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
  41. package/migrations/1722804019000_create_dashboard_table.js +69 -0
  42. package/migrations/1723665565000_prefs.js +59 -0
  43. package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
  44. package/migrations/1736640000000_custom_report_sorting.sql +7 -0
  45. package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
  46. package/migrations/1738491452000_sorting_rename.sql +13 -0
  47. package/migrations/1739139550000_bank_sync_page.sql +7 -0
  48. package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
  49. package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
  50. package/migrations/1749799110000_add_tags.sql +10 -0
  51. package/migrations/1749799110001_tags_tombstone.sql +5 -0
  52. package/migrations/1754611200000_add_category_template_settings.sql +5 -0
  53. package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
  54. package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
  55. package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
  56. package/migrations/1765518577215_multiple_dashboards.js +30 -0
  57. package/migrations/1768872504000_add_payee_locations.sql +21 -0
  58. package/package.json +128 -0
  59. package/src/mocks/arbitrary-schema.ts +162 -0
  60. package/src/mocks/budget.ts +901 -0
  61. package/src/mocks/files/8859-1.qfx +63 -0
  62. package/src/mocks/files/best.data-ever$.QFX +124 -0
  63. package/src/mocks/files/big.data.QiF +91 -0
  64. package/src/mocks/files/budgets/.commit-to-git +0 -0
  65. package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
  66. package/src/mocks/files/camt/camt.053.xml +463 -0
  67. package/src/mocks/files/credit-card.ofx +11 -0
  68. package/src/mocks/files/data-multi-decimal.ofx +64 -0
  69. package/src/mocks/files/data-payee-memo.ofx +75 -0
  70. package/src/mocks/files/data-payee-memo.qif +17 -0
  71. package/src/mocks/files/data.ofx +124 -0
  72. package/src/mocks/files/data.qfx +124 -0
  73. package/src/mocks/files/data.qif +91 -0
  74. package/src/mocks/files/default-budget-template/db.sqlite +0 -0
  75. package/src/mocks/files/default-budget-template/metadata.json +6 -0
  76. package/src/mocks/files/html-vals.qfx +17 -0
  77. package/src/mocks/index.ts +221 -0
  78. package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
  79. package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
  80. package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
  81. package/src/mocks/random.ts +16 -0
  82. package/src/mocks/setup.ts +180 -0
  83. package/src/mocks/spreadsheet.ts +101 -0
  84. package/src/mocks/util.ts +82 -0
  85. package/src/platform/client/connection/README.md +3 -0
  86. package/src/platform/client/connection/__mocks__/index.ts +67 -0
  87. package/src/platform/client/connection/index-types.ts +95 -0
  88. package/src/platform/client/connection/index.browser.ts +213 -0
  89. package/src/platform/client/connection/index.ts +155 -0
  90. package/src/platform/client/undo/index.ts +59 -0
  91. package/src/platform/exceptions/__mocks__/index.ts +7 -0
  92. package/src/platform/exceptions/index.ts +9 -0
  93. package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
  94. package/src/platform/server/asyncStorage/index-types.ts +35 -0
  95. package/src/platform/server/asyncStorage/index.api.ts +2 -0
  96. package/src/platform/server/asyncStorage/index.electron.ts +88 -0
  97. package/src/platform/server/asyncStorage/index.ts +126 -0
  98. package/src/platform/server/connection/README.md +3 -0
  99. package/src/platform/server/connection/__mocks__/index.ts +15 -0
  100. package/src/platform/server/connection/index-types.ts +20 -0
  101. package/src/platform/server/connection/index.api.ts +13 -0
  102. package/src/platform/server/connection/index.electron.ts +102 -0
  103. package/src/platform/server/connection/index.ts +154 -0
  104. package/src/platform/server/fetch/__mocks__/index.ts +3 -0
  105. package/src/platform/server/fetch/index.api.ts +1 -0
  106. package/src/platform/server/fetch/index.electron.ts +18 -0
  107. package/src/platform/server/fetch/index.ts +20 -0
  108. package/src/platform/server/fs/index.api.ts +198 -0
  109. package/src/platform/server/fs/index.electron.ts +208 -0
  110. package/src/platform/server/fs/index.test.ts +117 -0
  111. package/src/platform/server/fs/index.ts +416 -0
  112. package/src/platform/server/fs/path-join.api.ts +1 -0
  113. package/src/platform/server/fs/path-join.electron.ts +1 -0
  114. package/src/platform/server/fs/path-join.ts +97 -0
  115. package/src/platform/server/fs/shared.ts +33 -0
  116. package/src/platform/server/indexeddb/index.ts +115 -0
  117. package/src/platform/server/log/index.ts +43 -0
  118. package/src/platform/server/sqlite/index.api.ts +2 -0
  119. package/src/platform/server/sqlite/index.electron.ts +134 -0
  120. package/src/platform/server/sqlite/index.test.ts +108 -0
  121. package/src/platform/server/sqlite/index.ts +241 -0
  122. package/src/platform/server/sqlite/normalise.ts +9 -0
  123. package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
  124. package/src/platform/server/sqlite/unicodeLike.ts +31 -0
  125. package/src/server/__mocks__/post.ts +9 -0
  126. package/src/server/__snapshots__/main.test.ts.snap +199 -0
  127. package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
  128. package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
  129. package/src/server/accounts/app-bank-sync.test.ts +136 -0
  130. package/src/server/accounts/app.ts +1294 -0
  131. package/src/server/accounts/link.ts +25 -0
  132. package/src/server/accounts/payees.ts +36 -0
  133. package/src/server/accounts/sync.test.ts +679 -0
  134. package/src/server/accounts/sync.ts +1168 -0
  135. package/src/server/accounts/title/index.ts +60 -0
  136. package/src/server/accounts/title/lower-case.ts +93 -0
  137. package/src/server/accounts/title/specials.ts +21 -0
  138. package/src/server/admin/app.ts +241 -0
  139. package/src/server/api-models.ts +244 -0
  140. package/src/server/api.test.ts +36 -0
  141. package/src/server/api.ts +1030 -0
  142. package/src/server/app.ts +91 -0
  143. package/src/server/aql/compiler.test.ts +966 -0
  144. package/src/server/aql/compiler.ts +1222 -0
  145. package/src/server/aql/exec.test.ts +289 -0
  146. package/src/server/aql/exec.ts +128 -0
  147. package/src/server/aql/index.ts +41 -0
  148. package/src/server/aql/schema/executors.test.ts +420 -0
  149. package/src/server/aql/schema/executors.ts +345 -0
  150. package/src/server/aql/schema/index.test.ts +67 -0
  151. package/src/server/aql/schema/index.ts +409 -0
  152. package/src/server/aql/schema-helpers.test.ts +242 -0
  153. package/src/server/aql/schema-helpers.ts +208 -0
  154. package/src/server/aql/views.test.ts +62 -0
  155. package/src/server/aql/views.ts +57 -0
  156. package/src/server/auth/app.ts +387 -0
  157. package/src/server/bench.ts +29 -0
  158. package/src/server/budget/actions.ts +686 -0
  159. package/src/server/budget/app.ts +469 -0
  160. package/src/server/budget/base.test.ts +340 -0
  161. package/src/server/budget/base.ts +339 -0
  162. package/src/server/budget/category-template-context.test.ts +1658 -0
  163. package/src/server/budget/category-template-context.ts +862 -0
  164. package/src/server/budget/cleanup-template.pegjs +27 -0
  165. package/src/server/budget/cleanup-template.ts +408 -0
  166. package/src/server/budget/envelope.ts +403 -0
  167. package/src/server/budget/goal-template.pegjs +110 -0
  168. package/src/server/budget/goal-template.ts +309 -0
  169. package/src/server/budget/report.ts +308 -0
  170. package/src/server/budget/schedule-template.test.ts +184 -0
  171. package/src/server/budget/schedule-template.ts +351 -0
  172. package/src/server/budget/statements.ts +60 -0
  173. package/src/server/budget/template-notes.test.ts +393 -0
  174. package/src/server/budget/template-notes.ts +323 -0
  175. package/src/server/budget/util.ts +25 -0
  176. package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
  177. package/src/server/budgetfiles/app.ts +672 -0
  178. package/src/server/budgetfiles/backups.test.ts +79 -0
  179. package/src/server/budgetfiles/backups.ts +251 -0
  180. package/src/server/cloud-storage.ts +467 -0
  181. package/src/server/dashboard/app.ts +373 -0
  182. package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
  183. package/src/server/db/index.test.ts +300 -0
  184. package/src/server/db/index.ts +855 -0
  185. package/src/server/db/mappings.ts +59 -0
  186. package/src/server/db/sort.ts +58 -0
  187. package/src/server/db/types/index.ts +342 -0
  188. package/src/server/db/util.ts +36 -0
  189. package/src/server/encryption/app.ts +133 -0
  190. package/src/server/encryption/encryption-internals.api.ts +2 -0
  191. package/src/server/encryption/encryption-internals.electron.ts +89 -0
  192. package/src/server/encryption/encryption-internals.ts +109 -0
  193. package/src/server/encryption/encryption.test.ts +19 -0
  194. package/src/server/encryption/index.test.ts +19 -0
  195. package/src/server/encryption/index.ts +89 -0
  196. package/src/server/errors.ts +110 -0
  197. package/src/server/filters/app.ts +191 -0
  198. package/src/server/importers/actual.ts +49 -0
  199. package/src/server/importers/index.ts +58 -0
  200. package/src/server/importers/ynab4-types.ts +163 -0
  201. package/src/server/importers/ynab4.ts +470 -0
  202. package/src/server/importers/ynab5-types.ts +290 -0
  203. package/src/server/importers/ynab5.ts +1193 -0
  204. package/src/server/main-app.ts +25 -0
  205. package/src/server/main.test.ts +392 -0
  206. package/src/server/main.ts +336 -0
  207. package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
  208. package/src/server/migrate/cli.ts +100 -0
  209. package/src/server/migrate/migrations.test.ts +81 -0
  210. package/src/server/migrate/migrations.ts +192 -0
  211. package/src/server/models.ts +184 -0
  212. package/src/server/mutators.ts +139 -0
  213. package/src/server/notes/app.ts +18 -0
  214. package/src/server/payees/app.ts +351 -0
  215. package/src/server/polyfills.ts +26 -0
  216. package/src/server/post.ts +219 -0
  217. package/src/server/preferences/app.ts +249 -0
  218. package/src/server/prefs.ts +91 -0
  219. package/src/server/reports/app.ts +187 -0
  220. package/src/server/rules/action.ts +344 -0
  221. package/src/server/rules/app.ts +193 -0
  222. package/src/server/rules/condition.ts +436 -0
  223. package/src/server/rules/customFunctions.ts +61 -0
  224. package/src/server/rules/formula-action.test.ts +175 -0
  225. package/src/server/rules/handlebars-helpers.ts +131 -0
  226. package/src/server/rules/index.test.ts +1095 -0
  227. package/src/server/rules/index.ts +22 -0
  228. package/src/server/rules/rule-indexer.ts +89 -0
  229. package/src/server/rules/rule-utils.ts +274 -0
  230. package/src/server/rules/rule.ts +193 -0
  231. package/src/server/schedules/app.test.ts +502 -0
  232. package/src/server/schedules/app.ts +644 -0
  233. package/src/server/schedules/find-schedules.ts +391 -0
  234. package/src/server/server-config.ts +59 -0
  235. package/src/server/sheet.test.ts +101 -0
  236. package/src/server/sheet.ts +280 -0
  237. package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
  238. package/src/server/spreadsheet/app.ts +54 -0
  239. package/src/server/spreadsheet/globals.ts +13 -0
  240. package/src/server/spreadsheet/graph-data-structure.ts +165 -0
  241. package/src/server/spreadsheet/scratch +60 -0
  242. package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
  243. package/src/server/spreadsheet/spreadsheet.ts +523 -0
  244. package/src/server/spreadsheet/util.ts +15 -0
  245. package/src/server/sql/init.sql +88 -0
  246. package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
  247. package/src/server/sync/app.ts +29 -0
  248. package/src/server/sync/encoder.ts +129 -0
  249. package/src/server/sync/index.ts +820 -0
  250. package/src/server/sync/make-test-message.ts +19 -0
  251. package/src/server/sync/migrate.test.ts +169 -0
  252. package/src/server/sync/migrate.ts +48 -0
  253. package/src/server/sync/repair.ts +39 -0
  254. package/src/server/sync/reset.ts +91 -0
  255. package/src/server/sync/sync.property.test.ts +385 -0
  256. package/src/server/sync/sync.test.ts +349 -0
  257. package/src/server/sync/utils.ts +3 -0
  258. package/src/server/tags/app.ts +101 -0
  259. package/src/server/tests/mockData.json +9352 -0
  260. package/src/server/tests/mockSyncServer.ts +119 -0
  261. package/src/server/tools/app.ts +152 -0
  262. package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
  263. package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
  264. package/src/server/transactions/app.ts +136 -0
  265. package/src/server/transactions/export/export-to-csv.ts +132 -0
  266. package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
  267. package/src/server/transactions/import/ofx2json.test.ts +33 -0
  268. package/src/server/transactions/import/ofx2json.ts +157 -0
  269. package/src/server/transactions/import/parse-file.test.ts +224 -0
  270. package/src/server/transactions/import/parse-file.ts +286 -0
  271. package/src/server/transactions/import/qif2json.ts +110 -0
  272. package/src/server/transactions/import/xmlcamt2json.ts +168 -0
  273. package/src/server/transactions/index.ts +196 -0
  274. package/src/server/transactions/merge.test.ts +370 -0
  275. package/src/server/transactions/merge.ts +139 -0
  276. package/src/server/transactions/transaction-rules.test.ts +994 -0
  277. package/src/server/transactions/transaction-rules.ts +1038 -0
  278. package/src/server/transactions/transfer.test.ts +221 -0
  279. package/src/server/transactions/transfer.ts +173 -0
  280. package/src/server/undo.ts +271 -0
  281. package/src/server/update.ts +37 -0
  282. package/src/server/util/budget-name.ts +61 -0
  283. package/src/server/util/custom-sync-mapping.ts +48 -0
  284. package/src/server/util/rschedule.ts +9 -0
  285. package/src/shared/__mocks__/platform.ts +7 -0
  286. package/src/shared/__snapshots__/months.test.ts.snap +21 -0
  287. package/src/shared/arithmetic.test.ts +112 -0
  288. package/src/shared/arithmetic.ts +170 -0
  289. package/src/shared/async.test.ts +135 -0
  290. package/src/shared/async.ts +76 -0
  291. package/src/shared/constants.ts +5 -0
  292. package/src/shared/currencies.ts +70 -0
  293. package/src/shared/dashboard.ts +260 -0
  294. package/src/shared/environment.ts +18 -0
  295. package/src/shared/errors.ts +195 -0
  296. package/src/shared/locale.ts +27 -0
  297. package/src/shared/location-utils.test.ts +69 -0
  298. package/src/shared/location-utils.ts +49 -0
  299. package/src/shared/months.test.ts +5 -0
  300. package/src/shared/months.ts +485 -0
  301. package/src/shared/normalisation.ts +6 -0
  302. package/src/shared/platform.electron.ts +21 -0
  303. package/src/shared/platform.ts +20 -0
  304. package/src/shared/query.ts +176 -0
  305. package/src/shared/rules.test.ts +56 -0
  306. package/src/shared/rules.ts +371 -0
  307. package/src/shared/schedules.test.ts +570 -0
  308. package/src/shared/schedules.ts +560 -0
  309. package/src/shared/test-helpers.ts +156 -0
  310. package/src/shared/transactions.test.ts +275 -0
  311. package/src/shared/transactions.ts +433 -0
  312. package/src/shared/transfer.test.ts +75 -0
  313. package/src/shared/transfer.ts +16 -0
  314. package/src/shared/user.ts +4 -0
  315. package/src/shared/util.test.ts +240 -0
  316. package/src/shared/util.ts +633 -0
  317. package/src/types/api-handlers.ts +287 -0
  318. package/src/types/budget.ts +8 -0
  319. package/src/types/file.ts +47 -0
  320. package/src/types/handlers.ts +46 -0
  321. package/src/types/models/account.ts +24 -0
  322. package/src/types/models/bank-sync.ts +23 -0
  323. package/src/types/models/bank.ts +6 -0
  324. package/src/types/models/category-group.ts +11 -0
  325. package/src/types/models/category.ts +13 -0
  326. package/src/types/models/dashboard.ts +199 -0
  327. package/src/types/models/gocardless.ts +84 -0
  328. package/src/types/models/import-transaction.ts +56 -0
  329. package/src/types/models/index.ts +23 -0
  330. package/src/types/models/nearby-payee.ts +7 -0
  331. package/src/types/models/note.ts +4 -0
  332. package/src/types/models/openid.ts +8 -0
  333. package/src/types/models/payee-location.ts +8 -0
  334. package/src/types/models/payee.ts +10 -0
  335. package/src/types/models/pluggyai.ts +19 -0
  336. package/src/types/models/reports.ts +144 -0
  337. package/src/types/models/rule.ts +174 -0
  338. package/src/types/models/schedule.ts +49 -0
  339. package/src/types/models/simplefin.ts +28 -0
  340. package/src/types/models/tags.ts +6 -0
  341. package/src/types/models/templates.ts +135 -0
  342. package/src/types/models/transaction-filter.ts +9 -0
  343. package/src/types/models/transaction.ts +39 -0
  344. package/src/types/models/user-access.ts +10 -0
  345. package/src/types/models/user.ts +25 -0
  346. package/src/types/prefs.ts +167 -0
  347. package/src/types/server-events.ts +86 -0
  348. package/src/types/server-handlers.ts +27 -0
  349. package/src/types/util.ts +26 -0
  350. package/tsconfig.json +34 -0
  351. package/typings/pegjs.ts +1 -0
  352. package/typings/process-worker.ts +12 -0
  353. package/typings/vite-plugin-peggy-loader.ts +1 -0
  354. package/typings/window.ts +62 -0
  355. package/vite.config.ts +109 -0
  356. package/vite.desktop.config.ts +59 -0
  357. package/vitest.config.ts +43 -0
  358. package/vitest.web.config.ts +38 -0
@@ -0,0 +1,966 @@
1
+ // @ts-strict-ignore
2
+ import { q } from '../../shared/query';
3
+
4
+ import { generateSQLWithState } from './compiler';
5
+
6
+ function sqlLines(str) {
7
+ return str
8
+ .split('\n')
9
+ .filter(s => !s.match(/^\s*$/))
10
+ .map(line => line.trim());
11
+ }
12
+
13
+ const basicSchema = {
14
+ transactions: {
15
+ id: { type: 'id' },
16
+ date: { type: 'date' },
17
+ amount: { type: 'integer' },
18
+ amount2: { type: 'integer' },
19
+ amount3: { type: 'float' },
20
+ is_parent: { type: 'boolean' },
21
+ },
22
+ };
23
+
24
+ const schemaWithRefs = {
25
+ transactions: {
26
+ id: { type: 'id' },
27
+ payee: { type: 'id', ref: 'payees' },
28
+ date: { type: 'date' },
29
+ amount: { type: 'integer' },
30
+ },
31
+ payees: {
32
+ name: { type: 'string' },
33
+ id: { type: 'id' },
34
+ account: { type: 'id', ref: 'accounts' },
35
+ },
36
+ accounts: {
37
+ id: { type: 'id' },
38
+ trans1: { type: 'id', ref: 'transactions' },
39
+ trans2: { type: 'id', ref: 'transactions' },
40
+ trans3: { type: 'id', ref: 'transactions' },
41
+ },
42
+ };
43
+
44
+ const schemaWithTombstone = {
45
+ transactions: {
46
+ id: { type: 'id' },
47
+ payee: { type: 'id', ref: 'payees' },
48
+ amount: { type: 'integer' },
49
+ tombstone: { type: 'boolean' },
50
+ },
51
+ payees: {
52
+ name: { type: 'string' },
53
+ id: { type: 'id' },
54
+ tombstone: { type: 'boolean' },
55
+ },
56
+ accounts: {
57
+ id: { type: 'id' },
58
+ trans: { type: 'id', ref: 'transactions' },
59
+ },
60
+ };
61
+
62
+ describe('sheet language', () => {
63
+ it('`select` should select fields', () => {
64
+ let result = generateSQLWithState(
65
+ q('accounts')
66
+ .select(['trans1', 'trans2'])
67
+ .withoutValidatedRefs()
68
+ .serialize(),
69
+ schemaWithRefs,
70
+ );
71
+ expect(result.sql).toMatch(
72
+ 'SELECT accounts.trans1 AS trans1, accounts.trans2 AS trans2, accounts.id AS id FROM accounts',
73
+ );
74
+
75
+ // Allows renaming
76
+ result = generateSQLWithState(
77
+ q('accounts')
78
+ .select(['trans1', 'trans1.id', { transId: 'trans1.id' }])
79
+ .withoutValidatedRefs()
80
+ .serialize(),
81
+ schemaWithRefs,
82
+ );
83
+ expect(result.sql).toMatch(
84
+ 'SELECT accounts.trans1 AS trans1, transactions1.id AS "trans1.id", transactions1.id AS transId, accounts.id AS id FROM',
85
+ );
86
+
87
+ // Joined fields should be named by path
88
+ result = generateSQLWithState(
89
+ q('accounts')
90
+ .select(['trans1.payee.name'])
91
+ .withoutValidatedRefs()
92
+ .serialize(),
93
+ schemaWithRefs,
94
+ );
95
+ expect(result.sql).toMatch(
96
+ 'SELECT payees2.name AS "trans1.payee.name", accounts.id AS id FROM accounts',
97
+ );
98
+
99
+ // Renaming works with joined fields
100
+ result = generateSQLWithState(
101
+ q('accounts')
102
+ .select([{ payeeName: 'trans1.payee.name' }])
103
+ .withoutValidatedRefs()
104
+ .serialize(),
105
+ schemaWithRefs,
106
+ );
107
+ expect(result.sql).toMatch(
108
+ 'SELECT payees2.name AS payeeName, accounts.id AS id FROM accounts',
109
+ );
110
+
111
+ // By default, it should do id ref validation
112
+ result = generateSQLWithState(
113
+ q('accounts').select(['trans1', 'trans2']).serialize(),
114
+ schemaWithRefs,
115
+ );
116
+ expect(sqlLines(result.sql)).toEqual(
117
+ sqlLines(`
118
+ SELECT transactions1.id AS trans1, transactions2.id AS trans2, accounts.id AS id FROM accounts
119
+ LEFT JOIN transactions transactions1 ON transactions1.id = accounts.trans1
120
+ LEFT JOIN transactions transactions2 ON transactions2.id = accounts.trans2
121
+ WHERE 1
122
+ `),
123
+ );
124
+ });
125
+
126
+ it('`like` should use unicode and normalise function', () => {
127
+ const result = generateSQLWithState(
128
+ q('transactions')
129
+ .select('payee')
130
+ .filter({ 'payee.name': { $like: `%TEST%` } })
131
+ .serialize(),
132
+ schemaWithRefs,
133
+ );
134
+ expect(result.sql).toMatch(
135
+ `UNICODE_LIKE('%test%', NORMALISE(payees1.name))`,
136
+ );
137
+ });
138
+
139
+ it('`notlike` should use unicode and normalise function', () => {
140
+ const result = generateSQLWithState(
141
+ q('transactions')
142
+ .select('payee')
143
+ .filter({ 'payee.name': { $notlike: `%TEST%` } })
144
+ .serialize(),
145
+ schemaWithRefs,
146
+ );
147
+ expect(result.sql).toMatch(
148
+ `NOT UNICODE_LIKE('%test%', NORMALISE(payees1.name))`,
149
+ );
150
+ });
151
+
152
+ it('`select` allows nested functions', () => {
153
+ const result = generateSQLWithState(
154
+ q('transactions')
155
+ .select([{ num: { $idiv: [{ $neg: '$amount' }, 2] } }])
156
+ .serialize(),
157
+ schemaWithRefs,
158
+ );
159
+ expect(result.sql).toMatch(
160
+ 'SELECT ((-transactions.amount) / 2) AS num, transactions.id AS id FROM transactions',
161
+ );
162
+ });
163
+
164
+ it('`select` allows selecting all fields with *', () => {
165
+ let result = generateSQLWithState(
166
+ q('accounts').select(['*']).serialize(),
167
+ schemaWithRefs,
168
+ );
169
+ expect(sqlLines(result.sql)).toEqual(
170
+ sqlLines(`
171
+ SELECT accounts.id AS id, transactions1.id AS trans1, transactions2.id AS trans2, transactions3.id AS trans3 FROM accounts
172
+ LEFT JOIN transactions transactions1 ON transactions1.id = accounts.trans1
173
+ LEFT JOIN transactions transactions2 ON transactions2.id = accounts.trans2
174
+ LEFT JOIN transactions transactions3 ON transactions3.id = accounts.trans3
175
+ WHERE 1
176
+ `),
177
+ );
178
+
179
+ // Test selecting from joined tables
180
+ result = generateSQLWithState(
181
+ q('accounts').select(['*', 'trans1.*']).serialize(),
182
+ schemaWithRefs,
183
+ );
184
+ expect(sqlLines(result.sql)).toEqual(
185
+ sqlLines(`
186
+ SELECT accounts.id AS id, transactions1.id AS trans1, transactions2.id AS trans2, transactions3.id AS trans3, transactions1.id AS "trans1.id", payees4.id AS "trans1.payee", transactions1.date AS "trans1.date", transactions1.amount AS "trans1.amount" FROM accounts
187
+ LEFT JOIN transactions transactions1 ON transactions1.id = accounts.trans1
188
+ LEFT JOIN transactions transactions2 ON transactions2.id = accounts.trans2
189
+ LEFT JOIN transactions transactions3 ON transactions3.id = accounts.trans3
190
+ LEFT JOIN payees payees4 ON payees4.id = transactions1.payee
191
+ WHERE 1
192
+ `),
193
+ );
194
+ });
195
+
196
+ it('`select` excludes deleted rows by default and `withDead` includes them', () => {
197
+ // The tombstone flag is not added if not necessary (the table
198
+ // doesn't have it )
199
+ let result = generateSQLWithState(
200
+ q('accounts').select(['trans']).withoutValidatedRefs().serialize(),
201
+ schemaWithTombstone,
202
+ );
203
+ expect(result.sql).not.toMatch('tombstone');
204
+
205
+ // By default, the tombstone flag should be added if necessary
206
+ result = generateSQLWithState(
207
+ q('transactions').select(['amount']).serialize(),
208
+ schemaWithTombstone,
209
+ );
210
+ expect(sqlLines(result.sql)).toEqual(
211
+ sqlLines(`
212
+ SELECT transactions.amount AS amount, transactions.id AS id FROM transactions
213
+ WHERE 1 AND transactions.tombstone = 0
214
+ `),
215
+ );
216
+
217
+ // `withDead` should not add the tombstone flag
218
+ result = generateSQLWithState(
219
+ q('transactions').select(['amount']).withDead().serialize(),
220
+ schemaWithTombstone,
221
+ );
222
+ expect(sqlLines(result.sql)).toEqual(
223
+ sqlLines(`
224
+ SELECT transactions.amount AS amount, transactions.id AS id FROM transactions
225
+ WHERE 1
226
+ `),
227
+ );
228
+
229
+ // The tombstone flag should also be added if joining
230
+ result = generateSQLWithState(
231
+ q('accounts').select(['trans.amount', 'trans.payee.name']).serialize(),
232
+ schemaWithTombstone,
233
+ );
234
+ expect(sqlLines(result.sql)).toEqual(
235
+ sqlLines(`
236
+ SELECT transactions1.amount AS "trans.amount", payees2.name AS "trans.payee.name", accounts.id AS id FROM accounts
237
+ LEFT JOIN transactions transactions1 ON transactions1.id = accounts.trans AND transactions1.tombstone = 0
238
+ LEFT JOIN payees payees2 ON payees2.id = transactions1.payee AND payees2.tombstone = 0
239
+ WHERE 1
240
+ `),
241
+ );
242
+
243
+ // TODO: provide a way to customize joins, which would allow
244
+ // specifying include deleted
245
+ });
246
+
247
+ it('`select` always includes the id', () => {
248
+ let result = generateSQLWithState(
249
+ q('payees').select('name').serialize(),
250
+ schemaWithRefs,
251
+ );
252
+ expect(result.sql).toMatch('payees.id AS id');
253
+
254
+ result = generateSQLWithState(
255
+ q('payees').select(['name', 'id']).serialize(),
256
+ schemaWithRefs,
257
+ );
258
+ // id is only included once, we manually selected it
259
+ expect(result.sql).toMatch(
260
+ 'SELECT payees.name AS name, payees.id AS id FROM',
261
+ );
262
+
263
+ result = generateSQLWithState(
264
+ q('payees').select('name').groupBy('account').serialize(),
265
+ schemaWithRefs,
266
+ );
267
+ // id should not automatically by selected if using `groupBy`
268
+ expect(result.sql).not.toMatch('payees.id AS id');
269
+ });
270
+
271
+ it('automatically joins tables if referenced by path', () => {
272
+ // Join a simple table
273
+ let result = generateSQLWithState(
274
+ q('transactions')
275
+ .filter({ 'payee.name': 'kroger' })
276
+ .select(['amount'])
277
+ .serialize(),
278
+ schemaWithRefs,
279
+ );
280
+ expect([...result.state.paths.keys()]).toEqual(['transactions.payee']);
281
+ expect(sqlLines(result.sql)).toEqual(
282
+ sqlLines(`
283
+ SELECT transactions.amount AS amount, transactions.id AS id FROM transactions
284
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
285
+ WHERE (payees1.name = 'kroger')`),
286
+ );
287
+
288
+ // Make sure it works in a `get`
289
+ result = generateSQLWithState(
290
+ q('transactions')
291
+ .filter({ amount: 123 })
292
+ .select(['payee.name'])
293
+ .serialize(),
294
+ schemaWithRefs,
295
+ );
296
+ expect([...result.state.paths.keys()]).toEqual(['transactions.payee']);
297
+ expect(sqlLines(result.sql)).toEqual(
298
+ sqlLines(`
299
+ SELECT payees1.name AS "payee.name", transactions.id AS id FROM transactions
300
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
301
+ WHERE (transactions.amount = 123)
302
+ `),
303
+ );
304
+
305
+ // Join tables deeply
306
+ result = generateSQLWithState(
307
+ q('transactions')
308
+ .filter({ 'payee.account.trans1.amount': 234 })
309
+ .select(['amount', 'payee.name'])
310
+ .serialize(),
311
+ schemaWithRefs,
312
+ );
313
+ expect([...result.state.paths.keys()]).toEqual([
314
+ 'transactions.payee',
315
+ 'transactions.payee.account',
316
+ 'transactions.payee.account.trans1',
317
+ ]);
318
+ expect(sqlLines(result.sql)).toEqual(
319
+ sqlLines(`
320
+ SELECT transactions.amount AS amount, payees1.name AS "payee.name", transactions.id AS id FROM transactions
321
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
322
+ LEFT JOIN accounts accounts2 ON accounts2.id = payees1.account
323
+ LEFT JOIN transactions transactions3 ON transactions3.id = accounts2.trans1
324
+ WHERE (transactions3.amount = 234)
325
+ `),
326
+ );
327
+ });
328
+
329
+ it('avoids unnecessary joins when deeply joining', () => {
330
+ const { state, sql } = generateSQLWithState(
331
+ q('transactions')
332
+ .filter({
333
+ 'payee.account.trans1.amount': 1,
334
+ 'payee.account.trans2.amount': 2,
335
+ 'payee.account.trans3.amount': 3,
336
+ })
337
+ .select(['payee.account.trans2.payee'])
338
+ .serialize(),
339
+ schemaWithRefs,
340
+ );
341
+ expect([...state.paths.keys()]).toEqual([
342
+ 'transactions.payee',
343
+ 'transactions.payee.account',
344
+ 'transactions.payee.account.trans2',
345
+ 'transactions.payee.account.trans2.payee',
346
+ 'transactions.payee.account.trans1',
347
+ 'transactions.payee.account.trans3',
348
+ ]);
349
+ // It should not join `transactions.payee.account` multiple times,
350
+ // only once
351
+ expect(sqlLines(sql)).toEqual(
352
+ sqlLines(`
353
+ SELECT payees4.id AS "payee.account.trans2.payee", transactions.id AS id FROM transactions
354
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
355
+ LEFT JOIN accounts accounts2 ON accounts2.id = payees1.account
356
+ LEFT JOIN transactions transactions3 ON transactions3.id = accounts2.trans2
357
+ LEFT JOIN payees payees4 ON payees4.id = transactions3.payee
358
+ LEFT JOIN transactions transactions5 ON transactions5.id = accounts2.trans1
359
+ LEFT JOIN transactions transactions6 ON transactions6.id = accounts2.trans3
360
+ WHERE (transactions5.amount = 1
361
+ AND transactions3.amount = 2
362
+ AND transactions6.amount = 3)
363
+ `),
364
+ );
365
+ });
366
+
367
+ it('groupBy should work', () => {
368
+ let result = generateSQLWithState(
369
+ q('transactions').groupBy('payee.name').select('id').serialize(),
370
+ schemaWithRefs,
371
+ );
372
+ expect(sqlLines(result.sql)).toEqual(
373
+ sqlLines(`
374
+ SELECT transactions.id AS id FROM transactions
375
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
376
+ WHERE 1
377
+ GROUP BY payees1.name
378
+ `),
379
+ );
380
+
381
+ // Allows functions
382
+ result = generateSQLWithState(
383
+ q('transactions')
384
+ .groupBy({ $substr: ['$payee.name', 0, 4] })
385
+ .select('id')
386
+ .serialize(),
387
+ schemaWithRefs,
388
+ );
389
+ expect(result.sql).toMatch('GROUP BY SUBSTR(payees1.name, 0, 4)');
390
+ });
391
+
392
+ it('orderBy should work', () => {
393
+ let result = generateSQLWithState(
394
+ q('transactions').orderBy('payee.name').select('id').serialize(),
395
+ schemaWithRefs,
396
+ );
397
+ expect(sqlLines(result.sql)).toEqual(
398
+ sqlLines(`
399
+ SELECT transactions.id AS id FROM transactions
400
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
401
+ WHERE 1
402
+ ORDER BY payees1.name
403
+ `),
404
+ );
405
+
406
+ // Allows complex ordering and specifying direction
407
+ result = generateSQLWithState(
408
+ q('transactions')
409
+ .orderBy([
410
+ 'payee.id',
411
+ { 'payee.name': 'desc' },
412
+ { $substr: ['$payee.name', 0, 4] },
413
+ { $substr: ['$payee.name', 0, 4], $dir: 'desc' },
414
+ ])
415
+ .select('id')
416
+ .serialize(),
417
+ schemaWithRefs,
418
+ );
419
+ expect(result.sql).toMatch(
420
+ 'ORDER BY payees1.id, payees1.name desc, SUBSTR(payees1.name, 0, 4), SUBSTR(payees1.name, 0, 4) desc',
421
+ );
422
+ });
423
+
424
+ it('allows functions in `select`', () => {
425
+ let result = generateSQLWithState(
426
+ q('transactions')
427
+ .select(['id', { payeeName: { $substr: ['$payee.name', 0, 4] } }])
428
+ .serialize(),
429
+ schemaWithRefs,
430
+ );
431
+ expect(result.sql).toMatch(
432
+ 'SELECT transactions.id AS id, SUBSTR(payees1.name, 0, 4) AS payeeName FROM transactions',
433
+ );
434
+
435
+ result = generateSQLWithState(
436
+ q('transactions')
437
+ .select([
438
+ 'id',
439
+ { name: { $substr: [{ $substr: ['$payee.name', 1, 5] }, 3, 4] } },
440
+ ])
441
+ .serialize(),
442
+ schemaWithRefs,
443
+ );
444
+ expect(result.sql).toMatch(
445
+ 'SELECT transactions.id AS id, SUBSTR(SUBSTR(payees1.name, 1, 5), 3, 4) AS name FROM',
446
+ );
447
+ });
448
+
449
+ it('allows filtering with `filter`', () => {
450
+ let result = generateSQLWithState(
451
+ q('transactions')
452
+ .filter({
453
+ date: [{ $lt: '2020-01-01' }],
454
+ $or: [{ 'payee.name': 'foo' }, { 'payee.name': 'bar' }],
455
+ })
456
+ .select(['id'])
457
+ .serialize(),
458
+ schemaWithRefs,
459
+ );
460
+ expect(sqlLines(result.sql)).toEqual(
461
+ sqlLines(`
462
+ SELECT transactions.id AS id FROM transactions
463
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
464
+ WHERE (transactions.date < 20200101
465
+ AND (payees1.name = 'foo'
466
+ OR payees1.name = 'bar'))
467
+ `),
468
+ );
469
+
470
+ // Combining `$or` and `$and` works
471
+ result = generateSQLWithState(
472
+ q('transactions')
473
+ .filter({
474
+ $or: [
475
+ { 'payee.name': 'foo' },
476
+ { 'payee.name': 'bar' },
477
+ {
478
+ $and: [
479
+ { date: [{ $gt: '2019-12-31' }] },
480
+ { date: [{ $lt: '2020-01-01' }] },
481
+ ],
482
+ },
483
+ ],
484
+ })
485
+ .select(['id'])
486
+ .serialize(),
487
+ schemaWithRefs,
488
+ );
489
+ expect(sqlLines(result.sql)).toEqual(
490
+ sqlLines(`
491
+ SELECT transactions.id AS id FROM transactions
492
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
493
+ WHERE ((payees1.name = 'foo'
494
+ OR payees1.name = 'bar'
495
+ OR (transactions.date > 20191231
496
+ AND transactions.date < 20200101)))
497
+ `),
498
+ );
499
+
500
+ // Giving a field an array implicitly ANDs the filters
501
+ result = generateSQLWithState(
502
+ q('transactions')
503
+ .filter({ date: [{ $lt: '2020-01-01' }, { $gt: '2019-12-01' }] })
504
+ .select(['id'])
505
+ .serialize(),
506
+ schemaWithRefs,
507
+ );
508
+ expect(sqlLines(result.sql)).toEqual(
509
+ sqlLines(`
510
+ SELECT transactions.id AS id FROM transactions
511
+ WHERE (transactions.date < 20200101 AND transactions.date > 20191201)
512
+ `),
513
+ );
514
+
515
+ // Allows referencing fields
516
+ result = generateSQLWithState(
517
+ q('transactions')
518
+ .filter({ amount: { $lt: '$amount2' } })
519
+ .select(['id'])
520
+ .serialize(),
521
+ basicSchema,
522
+ );
523
+ expect(sqlLines(result.sql)).toEqual(
524
+ sqlLines(`
525
+ SELECT transactions.id AS id FROM transactions
526
+ WHERE (transactions.amount < transactions.amount2)
527
+ `),
528
+ );
529
+ });
530
+
531
+ it('$and and $or allow the object form', () => {
532
+ let result = generateSQLWithState(
533
+ q('transactions')
534
+ .filter({
535
+ $and: { payee: 'payee1', amount: 12 },
536
+ })
537
+ .select(['id'])
538
+ .withoutValidatedRefs()
539
+ .serialize(),
540
+ schemaWithRefs,
541
+ );
542
+ expect(result.sql).toMatch(
543
+ /WHERE \(\(transactions.payee = 'payee1'\s*\n\s*AND transactions.amount = 12\)\)/,
544
+ );
545
+
546
+ result = generateSQLWithState(
547
+ q('transactions')
548
+ .filter({
549
+ $or: { payee: 'payee1', amount: 12 },
550
+ })
551
+ .select(['id'])
552
+ .withoutValidatedRefs()
553
+ .serialize(),
554
+ schemaWithRefs,
555
+ );
556
+ expect(result.sql).toMatch(
557
+ /WHERE \(\(transactions.payee = 'payee1'\s*\n\s*OR transactions.amount = 12\)\)/,
558
+ );
559
+ });
560
+
561
+ it('allows functions in `filter`', () => {
562
+ // Allows transforming the input
563
+ let result = generateSQLWithState(
564
+ q('transactions')
565
+ .filter({
566
+ 'payee.name': { $transform: { $substr: ['$', 0, 4] }, $lt: 'foo' },
567
+ })
568
+ .select(['id'])
569
+ .serialize(),
570
+ schemaWithRefs,
571
+ );
572
+ expect(sqlLines(result.sql)).toEqual(
573
+ sqlLines(`
574
+ SELECT transactions.id AS id FROM transactions
575
+ LEFT JOIN payees payees1 ON payees1.id = transactions.payee
576
+ WHERE (SUBSTR(payees1.name, 0, 4) < 'foo')
577
+ `),
578
+ );
579
+
580
+ // Allows transforming left-hand side and calling a function on
581
+ // right-hand side
582
+ result = generateSQLWithState(
583
+ q('transactions')
584
+ .filter({
585
+ date: { $transform: '$month', $lt: { $month: '$date' } },
586
+ })
587
+ .select(['id'])
588
+ .serialize(),
589
+ schemaWithRefs,
590
+ );
591
+ expect(result.sql).toMatch(
592
+ 'WHERE (CAST(SUBSTR(transactions.date, 1, 6) AS integer) < CAST(SUBSTR(transactions.date, 1, 6) AS integer))',
593
+ );
594
+
595
+ // Allows nesting functions
596
+ result = generateSQLWithState(
597
+ q('transactions')
598
+ .filter({
599
+ 'payee.name': {
600
+ $lt: { $substr: [{ $substr: ['$payee.name', 1, 5] }, 3, 4] },
601
+ },
602
+ })
603
+ .select(['id'])
604
+ .serialize(),
605
+ schemaWithRefs,
606
+ );
607
+ expect(result.sql).toMatch(
608
+ 'WHERE (payees1.name < SUBSTR(SUBSTR(payees1.name, 1, 5), 3, 4))',
609
+ );
610
+ });
611
+
612
+ it('allows limit and offset', () => {
613
+ let result = generateSQLWithState(
614
+ q('transactions').select(['id']).limit(10).serialize(),
615
+ schemaWithRefs,
616
+ );
617
+ expect(result.sql).toMatch(/\s+LIMIT 10\s*$/);
618
+
619
+ result = generateSQLWithState(
620
+ q('transactions').select(['id']).offset(11).serialize(),
621
+ schemaWithRefs,
622
+ );
623
+ expect(result.sql).toMatch(/\s+OFFSET 11\s*$/);
624
+
625
+ result = generateSQLWithState(
626
+ q('transactions').select(['id']).limit(10).offset(11).serialize(),
627
+ schemaWithRefs,
628
+ );
629
+ expect(result.sql).toMatch(/\s+LIMIT 10\s*\n\s*OFFSET 11\s*$/);
630
+ });
631
+
632
+ it('allows named parameters', () => {
633
+ let result = generateSQLWithState(
634
+ q('transactions')
635
+ .filter({ amount: ':amount' })
636
+ .select(['id'])
637
+ .serialize(),
638
+ schemaWithRefs,
639
+ );
640
+ expect(result.sql).toMatch('transactions.amount = ?');
641
+
642
+ result = generateSQLWithState(
643
+ q('transactions')
644
+ .filter({ amount: { $lt: { $neg: ':amount' } } })
645
+ .select(['id'])
646
+ .serialize(),
647
+ schemaWithRefs,
648
+ );
649
+ expect(result.sql).toMatch('WHERE (transactions.amount < (-?))');
650
+
651
+ // Infers the right type
652
+ result = generateSQLWithState(
653
+ q('transactions')
654
+ .filter({ date: { $transform: '$month', $eq: { $month: ':month' } } })
655
+ .select()
656
+ .serialize(),
657
+ schemaWithRefs,
658
+ );
659
+ const monthParam = result.state.namedParameters.find(
660
+ p => p.paramName === 'month',
661
+ );
662
+ expect(monthParam.paramType).toBe('date-month');
663
+ });
664
+
665
+ it('allows customizing generated SQL', () => {
666
+ let result = generateSQLWithState(
667
+ q('transactions').select(['amount']).serialize(),
668
+ schemaWithRefs,
669
+ {
670
+ tableViews: { transactions: 'v_transactions' },
671
+ tableFilters: name =>
672
+ name === 'transactions' ? [{ amount: { $gt: 0 } }] : [],
673
+ },
674
+ );
675
+ expect(sqlLines(result.sql)).toEqual(
676
+ sqlLines(`
677
+ SELECT v_transactions.amount AS amount, v_transactions.id AS id FROM v_transactions
678
+ WHERE 1 AND (v_transactions.amount > 0)
679
+ `),
680
+ );
681
+
682
+ // Make sure the same customizations are applied when joining
683
+ result = generateSQLWithState(
684
+ q('accounts').select(['trans1.amount']).serialize(),
685
+ schemaWithRefs,
686
+ {
687
+ tableViews: { transactions: 'v_transactions' },
688
+ tableFilters: name =>
689
+ name === 'transactions' ? [{ amount: { $gt: 0 } }] : [],
690
+ },
691
+ );
692
+ // The joined table should be customized
693
+ expect(result.sql).toMatch('LEFT JOIN v_transactions');
694
+ // Make sure the filter works on the joined table, not the
695
+ // implicit table (it has a "1" suffix)
696
+ expect(result.sql).toMatch('transactions1.amount > 0');
697
+ // Check the entire sql
698
+ expect(sqlLines(result.sql)).toEqual(
699
+ sqlLines(`
700
+ SELECT transactions1.amount AS "trans1.amount", accounts.id AS id FROM accounts
701
+ LEFT JOIN v_transactions transactions1 ON transactions1.id = accounts.trans1 AND (transactions1.amount > 0)
702
+ WHERE 1
703
+ `),
704
+ );
705
+
706
+ // Internal table filters can't use paths
707
+ expect(() =>
708
+ generateSQLWithState(
709
+ q('accounts').select(['trans1.amount']).serialize(),
710
+ schemaWithRefs,
711
+ {
712
+ tableViews: { transactions: 'v_transactions' },
713
+ tableFilters: name =>
714
+ name === 'transactions' ? [{ 'payee.name': 'foo' }] : [],
715
+ },
716
+ ),
717
+ ).toThrow(/cannot contain paths/);
718
+ });
719
+
720
+ it('raw mode avoids any internal filters', () => {
721
+ const result = generateSQLWithState(
722
+ q('transactions').select(['amount']).raw().serialize(),
723
+ schemaWithRefs,
724
+ {
725
+ tableViews: { transactions: 'v_transactions' },
726
+ tableFilters: name =>
727
+ name === 'transactions' ? [{ amount: { $gt: 0 } }] : [],
728
+ },
729
+ );
730
+ expect(sqlLines(result.sql)).toEqual(
731
+ sqlLines(`
732
+ SELECT v_transactions.amount AS amount, v_transactions.id AS id FROM v_transactions
733
+ WHERE 1
734
+ `),
735
+ );
736
+ });
737
+
738
+ it('tracks compiler state for debugging', () => {
739
+ // select
740
+ try {
741
+ generateSQLWithState(
742
+ q('transactions')
743
+ .select({ month: { $month: '$payee.name2' } })
744
+ .serialize(),
745
+ schemaWithRefs,
746
+ );
747
+ throw new Error('Test should have thrown');
748
+ } catch (e) {
749
+ expect(e.message).toMatch('Expression stack:');
750
+ expect(e.message).toMatch(/\$payee.name2\n/g);
751
+ expect(e.message).toMatch('{"$month":"$payee.name2"}');
752
+ expect(e.message).toMatch('select({"month":{"$month":"$payee.name2"}})');
753
+ }
754
+
755
+ // filter
756
+ try {
757
+ generateSQLWithState(
758
+ q('transactions')
759
+ .filter({ date: { $transform: '$month', $eq: 10 } })
760
+ .select(['id'])
761
+ .serialize(),
762
+ schemaWithRefs,
763
+ );
764
+ throw new Error('Test should have thrown');
765
+ } catch (e) {
766
+ expect(e.message).toMatch('Expression stack:');
767
+ expect(e.message).toMatch('{"date":{"$transform":"$month","$eq":10}}');
768
+ expect(e.message).toMatch(
769
+ 'filter({"date":{"$transform":"$month","$eq":10}})',
770
+ );
771
+ }
772
+
773
+ // group by
774
+ try {
775
+ generateSQLWithState(
776
+ q('transactions')
777
+ .groupBy({ $month: '$date2' })
778
+ .select({ amount: { $sum: '$amount' } })
779
+ .serialize(),
780
+ schemaWithRefs,
781
+ );
782
+ throw new Error('Test should have thrown');
783
+ } catch (e) {
784
+ expect(e.message).toMatch('Expression stack:');
785
+ expect(e.message).toMatch(/\$date2\n/g);
786
+ expect(e.message).toMatch('{"$month":"$date2"}');
787
+ expect(e.message).toMatch('groupBy({"$month":"$date2"})');
788
+ }
789
+
790
+ // order by
791
+ try {
792
+ generateSQLWithState(
793
+ q('transactions')
794
+ .orderBy({ $month: '$date2' })
795
+ .select({ amount: { $sum: '$amount' } })
796
+ .serialize(),
797
+ schemaWithRefs,
798
+ );
799
+ throw new Error('Test should have thrown');
800
+ } catch (e) {
801
+ expect(e.message).toMatch('Expression stack:');
802
+ expect(e.message).toMatch(/\$date2\n/g);
803
+ expect(e.message).toMatch('{"$month":"$date2"}');
804
+ expect(e.message).toMatch('orderBy({"$month":"$date2"})');
805
+ }
806
+ });
807
+
808
+ it('$oneof creates template for executor to run', () => {
809
+ const result = generateSQLWithState(
810
+ q('transactions')
811
+ .filter({ id: { $oneof: ['one', 'two', 'three'] } })
812
+ .select(['amount'])
813
+ .serialize(),
814
+ schemaWithRefs,
815
+ );
816
+ expect(result.sql).toMatch("id IN ('one','two','three')");
817
+ });
818
+ });
819
+
820
+ describe('Type conversions', () => {
821
+ it('date literals are converted to ints on input', () => {
822
+ let result = generateSQLWithState(
823
+ q('transactions')
824
+ .filter({ date: '2020-01-01' })
825
+ .select(['id'])
826
+ .serialize(),
827
+ basicSchema,
828
+ );
829
+ expect(result.sql).toMatch('WHERE (transactions.date = 20200101)');
830
+
831
+ result = generateSQLWithState(
832
+ q('transactions')
833
+ .filter({ date: { $transform: '$month', $eq: '2020-01' } })
834
+ .select(['id'])
835
+ .serialize(),
836
+ basicSchema,
837
+ );
838
+ expect(result.sql).toMatch(
839
+ 'WHERE (CAST(SUBSTR(transactions.date, 1, 6) AS integer) = 202001)',
840
+ );
841
+
842
+ // You can also specify a full date that is auto-converted to month
843
+ result = generateSQLWithState(
844
+ q('transactions')
845
+ .filter({ date: { $transform: '$month', $eq: '2020-01-01' } })
846
+ .select(['id'])
847
+ .serialize(),
848
+ basicSchema,
849
+ );
850
+ expect(result.sql).toMatch(
851
+ 'WHERE (CAST(SUBSTR(transactions.date, 1, 6) AS integer) = 202001)',
852
+ );
853
+
854
+ // You can also specify a full date that is auto-converted to month
855
+ result = generateSQLWithState(
856
+ q('transactions')
857
+ .filter({ date: { $transform: '$year', $eq: '2020-01-01' } })
858
+ .select(['id'])
859
+ .serialize(),
860
+ basicSchema,
861
+ );
862
+ expect(result.sql).toMatch(
863
+ 'WHERE (CAST(SUBSTR(transactions.date, 1, 4) AS integer) = 2020)',
864
+ );
865
+ });
866
+
867
+ it('date fields are converted to months and years', () => {
868
+ let result = generateSQLWithState(
869
+ q('accounts')
870
+ .filter({
871
+ 'trans1.date': { $transform: '$month', $eq: '$trans2.date' },
872
+ })
873
+ .select(['id'])
874
+ .serialize(),
875
+ schemaWithRefs,
876
+ );
877
+ expect(result.sql).toMatch(
878
+ 'WHERE (CAST(SUBSTR(transactions2.date, 1, 6) AS integer) = CAST(SUBSTR(transactions1.date, 1, 6) AS integer))',
879
+ );
880
+
881
+ // You can also specify a full date that is auto-converted to month
882
+ result = generateSQLWithState(
883
+ q('accounts')
884
+ .filter({ 'trans1.date': { $transform: '$year', $eq: '$trans2.date' } })
885
+ .select(['id'])
886
+ .serialize(),
887
+ schemaWithRefs,
888
+ );
889
+ expect(result.sql).toMatch(
890
+ 'WHERE (CAST(SUBSTR(transactions2.date, 1, 4) AS integer) = CAST(SUBSTR(transactions1.date, 1, 4) AS integer))',
891
+ );
892
+ });
893
+
894
+ it('allows conversions from string to id', () => {
895
+ expect(() => {
896
+ generateSQLWithState(
897
+ q('transactions').filter({ id: 'foo' }).select(['id']).serialize(),
898
+ schemaWithRefs,
899
+ );
900
+ }).not.toThrow();
901
+
902
+ expect(() => {
903
+ generateSQLWithState(
904
+ q('accounts').filter({ id: '$trans1.id' }).select(['id']).serialize(),
905
+ schemaWithRefs,
906
+ );
907
+ }).not.toThrow();
908
+
909
+ // Numbers cannot be converted to ids
910
+ expect(() => {
911
+ generateSQLWithState(
912
+ q('transactions').filter({ id: 5 }).select(['id']).serialize(),
913
+ schemaWithRefs,
914
+ );
915
+ }).toThrow(/Can't convert/);
916
+ });
917
+
918
+ it('allows conversions from integers to floats', () => {
919
+ expect(() => {
920
+ generateSQLWithState(
921
+ q('transactions').filter({ amount3: 45 }).select(['id']).serialize(),
922
+ basicSchema,
923
+ );
924
+ }).not.toThrow();
925
+
926
+ // Floats cannot be converted to ints
927
+ expect(() => {
928
+ generateSQLWithState(
929
+ q('transactions').filter({ amount: 45.5 }).select(['id']).serialize(),
930
+ basicSchema,
931
+ );
932
+ }).toThrow(/Can't convert/);
933
+ });
934
+
935
+ it('allows fields to be nullable', () => {
936
+ // With validated refs
937
+ let result = generateSQLWithState(
938
+ q('transactions').filter({ payee: null }).select().serialize(),
939
+ schemaWithRefs,
940
+ );
941
+ expect(result.sql).toMatch('WHERE (payees1.id IS NULL)');
942
+
943
+ // Without validated refs
944
+ result = generateSQLWithState(
945
+ q('transactions')
946
+ .filter({ payee: null })
947
+ .select()
948
+ .withoutValidatedRefs()
949
+ .serialize(),
950
+ schemaWithRefs,
951
+ );
952
+ expect(result.sql).toMatch('WHERE (transactions.payee IS NULL)');
953
+ });
954
+
955
+ it('allows fields to be not nullable', () => {
956
+ // With validated refs
957
+ const result = generateSQLWithState(
958
+ q('transactions')
959
+ .filter({ payee: { $ne: null } })
960
+ .select()
961
+ .serialize(),
962
+ schemaWithRefs,
963
+ );
964
+ expect(result.sql).toMatch('WHERE (payees1.id IS NOT NULL)');
965
+ });
966
+ });