@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.
- package/.swcrc +11 -0
- package/bin/build-browser +40 -0
- package/bin/copy-migrations +9 -0
- package/db.sqlite +0 -0
- package/default-db.sqlite +0 -0
- package/migrations/.force-copy-windows +0 -0
- package/migrations/1548957970627_remove-db-version.sql +5 -0
- package/migrations/1550601598648_payees.sql +23 -0
- package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
- package/migrations/1561751833510_indexes.sql +7 -0
- package/migrations/1567699552727_budget.sql +38 -0
- package/migrations/1582384163573_cleared.sql +6 -0
- package/migrations/1597756566448_rules.sql +10 -0
- package/migrations/1608652596043_parent_field.sql +13 -0
- package/migrations/1608652596044_trans_views.sql +56 -0
- package/migrations/1612625548236_optimize.sql +7 -0
- package/migrations/1614782639336_trans_views2.sql +33 -0
- package/migrations/1615745967948_meta.sql +10 -0
- package/migrations/1616167010796_accounts_order.sql +5 -0
- package/migrations/1618975177358_schedules.sql +28 -0
- package/migrations/1632571489012_remove_cache.js +136 -0
- package/migrations/1679728867040_rules_conditions.sql +5 -0
- package/migrations/1681115033845_add_schedule_name.sql +5 -0
- package/migrations/1682974838138_remove_payee_rules.sql +5 -0
- package/migrations/1685007876842_add_category_hidden.sql +6 -0
- package/migrations/1686139660866_remove_account_type.sql +5 -0
- package/migrations/1688749527273_transaction_filters.sql +10 -0
- package/migrations/1688841238000_add_account_type.sql +5 -0
- package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
- package/migrations/1694438752000_add_goal_targets.sql +7 -0
- package/migrations/1697046240000_add_reconciled.sql +5 -0
- package/migrations/1704572023730_add_account_sync_source.sql +5 -0
- package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
- package/migrations/1707267033000_reports.sql +28 -0
- package/migrations/1712784523000_unhide_input_group.sql +8 -0
- package/migrations/1716359441000_include_current.sql +5 -0
- package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
- package/migrations/1720664867241_add_payee_favorite.sql +5 -0
- package/migrations/1720665000000_goal_context.sql +6 -0
- package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
- package/migrations/1722804019000_create_dashboard_table.js +69 -0
- package/migrations/1723665565000_prefs.js +59 -0
- package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
- package/migrations/1736640000000_custom_report_sorting.sql +7 -0
- package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
- package/migrations/1738491452000_sorting_rename.sql +13 -0
- package/migrations/1739139550000_bank_sync_page.sql +7 -0
- package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
- package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
- package/migrations/1749799110000_add_tags.sql +10 -0
- package/migrations/1749799110001_tags_tombstone.sql +5 -0
- package/migrations/1754611200000_add_category_template_settings.sql +5 -0
- package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
- package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
- package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
- package/migrations/1765518577215_multiple_dashboards.js +30 -0
- package/migrations/1768872504000_add_payee_locations.sql +21 -0
- package/package.json +128 -0
- package/src/mocks/arbitrary-schema.ts +162 -0
- package/src/mocks/budget.ts +901 -0
- package/src/mocks/files/8859-1.qfx +63 -0
- package/src/mocks/files/best.data-ever$.QFX +124 -0
- package/src/mocks/files/big.data.QiF +91 -0
- package/src/mocks/files/budgets/.commit-to-git +0 -0
- package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
- package/src/mocks/files/camt/camt.053.xml +463 -0
- package/src/mocks/files/credit-card.ofx +11 -0
- package/src/mocks/files/data-multi-decimal.ofx +64 -0
- package/src/mocks/files/data-payee-memo.ofx +75 -0
- package/src/mocks/files/data-payee-memo.qif +17 -0
- package/src/mocks/files/data.ofx +124 -0
- package/src/mocks/files/data.qfx +124 -0
- package/src/mocks/files/data.qif +91 -0
- package/src/mocks/files/default-budget-template/db.sqlite +0 -0
- package/src/mocks/files/default-budget-template/metadata.json +6 -0
- package/src/mocks/files/html-vals.qfx +17 -0
- package/src/mocks/index.ts +221 -0
- package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
- package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
- package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
- package/src/mocks/random.ts +16 -0
- package/src/mocks/setup.ts +180 -0
- package/src/mocks/spreadsheet.ts +101 -0
- package/src/mocks/util.ts +82 -0
- package/src/platform/client/connection/README.md +3 -0
- package/src/platform/client/connection/__mocks__/index.ts +67 -0
- package/src/platform/client/connection/index-types.ts +95 -0
- package/src/platform/client/connection/index.browser.ts +213 -0
- package/src/platform/client/connection/index.ts +155 -0
- package/src/platform/client/undo/index.ts +59 -0
- package/src/platform/exceptions/__mocks__/index.ts +7 -0
- package/src/platform/exceptions/index.ts +9 -0
- package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
- package/src/platform/server/asyncStorage/index-types.ts +35 -0
- package/src/platform/server/asyncStorage/index.api.ts +2 -0
- package/src/platform/server/asyncStorage/index.electron.ts +88 -0
- package/src/platform/server/asyncStorage/index.ts +126 -0
- package/src/platform/server/connection/README.md +3 -0
- package/src/platform/server/connection/__mocks__/index.ts +15 -0
- package/src/platform/server/connection/index-types.ts +20 -0
- package/src/platform/server/connection/index.api.ts +13 -0
- package/src/platform/server/connection/index.electron.ts +102 -0
- package/src/platform/server/connection/index.ts +154 -0
- package/src/platform/server/fetch/__mocks__/index.ts +3 -0
- package/src/platform/server/fetch/index.api.ts +1 -0
- package/src/platform/server/fetch/index.electron.ts +18 -0
- package/src/platform/server/fetch/index.ts +20 -0
- package/src/platform/server/fs/index.api.ts +198 -0
- package/src/platform/server/fs/index.electron.ts +208 -0
- package/src/platform/server/fs/index.test.ts +117 -0
- package/src/platform/server/fs/index.ts +416 -0
- package/src/platform/server/fs/path-join.api.ts +1 -0
- package/src/platform/server/fs/path-join.electron.ts +1 -0
- package/src/platform/server/fs/path-join.ts +97 -0
- package/src/platform/server/fs/shared.ts +33 -0
- package/src/platform/server/indexeddb/index.ts +115 -0
- package/src/platform/server/log/index.ts +43 -0
- package/src/platform/server/sqlite/index.api.ts +2 -0
- package/src/platform/server/sqlite/index.electron.ts +134 -0
- package/src/platform/server/sqlite/index.test.ts +108 -0
- package/src/platform/server/sqlite/index.ts +241 -0
- package/src/platform/server/sqlite/normalise.ts +9 -0
- package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
- package/src/platform/server/sqlite/unicodeLike.ts +31 -0
- package/src/server/__mocks__/post.ts +9 -0
- package/src/server/__snapshots__/main.test.ts.snap +199 -0
- package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
- package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
- package/src/server/accounts/app-bank-sync.test.ts +136 -0
- package/src/server/accounts/app.ts +1294 -0
- package/src/server/accounts/link.ts +25 -0
- package/src/server/accounts/payees.ts +36 -0
- package/src/server/accounts/sync.test.ts +679 -0
- package/src/server/accounts/sync.ts +1168 -0
- package/src/server/accounts/title/index.ts +60 -0
- package/src/server/accounts/title/lower-case.ts +93 -0
- package/src/server/accounts/title/specials.ts +21 -0
- package/src/server/admin/app.ts +241 -0
- package/src/server/api-models.ts +244 -0
- package/src/server/api.test.ts +36 -0
- package/src/server/api.ts +1030 -0
- package/src/server/app.ts +91 -0
- package/src/server/aql/compiler.test.ts +966 -0
- package/src/server/aql/compiler.ts +1222 -0
- package/src/server/aql/exec.test.ts +289 -0
- package/src/server/aql/exec.ts +128 -0
- package/src/server/aql/index.ts +41 -0
- package/src/server/aql/schema/executors.test.ts +420 -0
- package/src/server/aql/schema/executors.ts +345 -0
- package/src/server/aql/schema/index.test.ts +67 -0
- package/src/server/aql/schema/index.ts +409 -0
- package/src/server/aql/schema-helpers.test.ts +242 -0
- package/src/server/aql/schema-helpers.ts +208 -0
- package/src/server/aql/views.test.ts +62 -0
- package/src/server/aql/views.ts +57 -0
- package/src/server/auth/app.ts +387 -0
- package/src/server/bench.ts +29 -0
- package/src/server/budget/actions.ts +686 -0
- package/src/server/budget/app.ts +469 -0
- package/src/server/budget/base.test.ts +340 -0
- package/src/server/budget/base.ts +339 -0
- package/src/server/budget/category-template-context.test.ts +1658 -0
- package/src/server/budget/category-template-context.ts +862 -0
- package/src/server/budget/cleanup-template.pegjs +27 -0
- package/src/server/budget/cleanup-template.ts +408 -0
- package/src/server/budget/envelope.ts +403 -0
- package/src/server/budget/goal-template.pegjs +110 -0
- package/src/server/budget/goal-template.ts +309 -0
- package/src/server/budget/report.ts +308 -0
- package/src/server/budget/schedule-template.test.ts +184 -0
- package/src/server/budget/schedule-template.ts +351 -0
- package/src/server/budget/statements.ts +60 -0
- package/src/server/budget/template-notes.test.ts +393 -0
- package/src/server/budget/template-notes.ts +323 -0
- package/src/server/budget/util.ts +25 -0
- package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
- package/src/server/budgetfiles/app.ts +672 -0
- package/src/server/budgetfiles/backups.test.ts +79 -0
- package/src/server/budgetfiles/backups.ts +251 -0
- package/src/server/cloud-storage.ts +467 -0
- package/src/server/dashboard/app.ts +373 -0
- package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
- package/src/server/db/index.test.ts +300 -0
- package/src/server/db/index.ts +855 -0
- package/src/server/db/mappings.ts +59 -0
- package/src/server/db/sort.ts +58 -0
- package/src/server/db/types/index.ts +342 -0
- package/src/server/db/util.ts +36 -0
- package/src/server/encryption/app.ts +133 -0
- package/src/server/encryption/encryption-internals.api.ts +2 -0
- package/src/server/encryption/encryption-internals.electron.ts +89 -0
- package/src/server/encryption/encryption-internals.ts +109 -0
- package/src/server/encryption/encryption.test.ts +19 -0
- package/src/server/encryption/index.test.ts +19 -0
- package/src/server/encryption/index.ts +89 -0
- package/src/server/errors.ts +110 -0
- package/src/server/filters/app.ts +191 -0
- package/src/server/importers/actual.ts +49 -0
- package/src/server/importers/index.ts +58 -0
- package/src/server/importers/ynab4-types.ts +163 -0
- package/src/server/importers/ynab4.ts +470 -0
- package/src/server/importers/ynab5-types.ts +290 -0
- package/src/server/importers/ynab5.ts +1193 -0
- package/src/server/main-app.ts +25 -0
- package/src/server/main.test.ts +392 -0
- package/src/server/main.ts +336 -0
- package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
- package/src/server/migrate/cli.ts +100 -0
- package/src/server/migrate/migrations.test.ts +81 -0
- package/src/server/migrate/migrations.ts +192 -0
- package/src/server/models.ts +184 -0
- package/src/server/mutators.ts +139 -0
- package/src/server/notes/app.ts +18 -0
- package/src/server/payees/app.ts +351 -0
- package/src/server/polyfills.ts +26 -0
- package/src/server/post.ts +219 -0
- package/src/server/preferences/app.ts +249 -0
- package/src/server/prefs.ts +91 -0
- package/src/server/reports/app.ts +187 -0
- package/src/server/rules/action.ts +344 -0
- package/src/server/rules/app.ts +193 -0
- package/src/server/rules/condition.ts +436 -0
- package/src/server/rules/customFunctions.ts +61 -0
- package/src/server/rules/formula-action.test.ts +175 -0
- package/src/server/rules/handlebars-helpers.ts +131 -0
- package/src/server/rules/index.test.ts +1095 -0
- package/src/server/rules/index.ts +22 -0
- package/src/server/rules/rule-indexer.ts +89 -0
- package/src/server/rules/rule-utils.ts +274 -0
- package/src/server/rules/rule.ts +193 -0
- package/src/server/schedules/app.test.ts +502 -0
- package/src/server/schedules/app.ts +644 -0
- package/src/server/schedules/find-schedules.ts +391 -0
- package/src/server/server-config.ts +59 -0
- package/src/server/sheet.test.ts +101 -0
- package/src/server/sheet.ts +280 -0
- package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
- package/src/server/spreadsheet/app.ts +54 -0
- package/src/server/spreadsheet/globals.ts +13 -0
- package/src/server/spreadsheet/graph-data-structure.ts +165 -0
- package/src/server/spreadsheet/scratch +60 -0
- package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
- package/src/server/spreadsheet/spreadsheet.ts +523 -0
- package/src/server/spreadsheet/util.ts +15 -0
- package/src/server/sql/init.sql +88 -0
- package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
- package/src/server/sync/app.ts +29 -0
- package/src/server/sync/encoder.ts +129 -0
- package/src/server/sync/index.ts +820 -0
- package/src/server/sync/make-test-message.ts +19 -0
- package/src/server/sync/migrate.test.ts +169 -0
- package/src/server/sync/migrate.ts +48 -0
- package/src/server/sync/repair.ts +39 -0
- package/src/server/sync/reset.ts +91 -0
- package/src/server/sync/sync.property.test.ts +385 -0
- package/src/server/sync/sync.test.ts +349 -0
- package/src/server/sync/utils.ts +3 -0
- package/src/server/tags/app.ts +101 -0
- package/src/server/tests/mockData.json +9352 -0
- package/src/server/tests/mockSyncServer.ts +119 -0
- package/src/server/tools/app.ts +152 -0
- package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
- package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
- package/src/server/transactions/app.ts +136 -0
- package/src/server/transactions/export/export-to-csv.ts +132 -0
- package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
- package/src/server/transactions/import/ofx2json.test.ts +33 -0
- package/src/server/transactions/import/ofx2json.ts +157 -0
- package/src/server/transactions/import/parse-file.test.ts +224 -0
- package/src/server/transactions/import/parse-file.ts +286 -0
- package/src/server/transactions/import/qif2json.ts +110 -0
- package/src/server/transactions/import/xmlcamt2json.ts +168 -0
- package/src/server/transactions/index.ts +196 -0
- package/src/server/transactions/merge.test.ts +370 -0
- package/src/server/transactions/merge.ts +139 -0
- package/src/server/transactions/transaction-rules.test.ts +994 -0
- package/src/server/transactions/transaction-rules.ts +1038 -0
- package/src/server/transactions/transfer.test.ts +221 -0
- package/src/server/transactions/transfer.ts +173 -0
- package/src/server/undo.ts +271 -0
- package/src/server/update.ts +37 -0
- package/src/server/util/budget-name.ts +61 -0
- package/src/server/util/custom-sync-mapping.ts +48 -0
- package/src/server/util/rschedule.ts +9 -0
- package/src/shared/__mocks__/platform.ts +7 -0
- package/src/shared/__snapshots__/months.test.ts.snap +21 -0
- package/src/shared/arithmetic.test.ts +112 -0
- package/src/shared/arithmetic.ts +170 -0
- package/src/shared/async.test.ts +135 -0
- package/src/shared/async.ts +76 -0
- package/src/shared/constants.ts +5 -0
- package/src/shared/currencies.ts +70 -0
- package/src/shared/dashboard.ts +260 -0
- package/src/shared/environment.ts +18 -0
- package/src/shared/errors.ts +195 -0
- package/src/shared/locale.ts +27 -0
- package/src/shared/location-utils.test.ts +69 -0
- package/src/shared/location-utils.ts +49 -0
- package/src/shared/months.test.ts +5 -0
- package/src/shared/months.ts +485 -0
- package/src/shared/normalisation.ts +6 -0
- package/src/shared/platform.electron.ts +21 -0
- package/src/shared/platform.ts +20 -0
- package/src/shared/query.ts +176 -0
- package/src/shared/rules.test.ts +56 -0
- package/src/shared/rules.ts +371 -0
- package/src/shared/schedules.test.ts +570 -0
- package/src/shared/schedules.ts +560 -0
- package/src/shared/test-helpers.ts +156 -0
- package/src/shared/transactions.test.ts +275 -0
- package/src/shared/transactions.ts +433 -0
- package/src/shared/transfer.test.ts +75 -0
- package/src/shared/transfer.ts +16 -0
- package/src/shared/user.ts +4 -0
- package/src/shared/util.test.ts +240 -0
- package/src/shared/util.ts +633 -0
- package/src/types/api-handlers.ts +287 -0
- package/src/types/budget.ts +8 -0
- package/src/types/file.ts +47 -0
- package/src/types/handlers.ts +46 -0
- package/src/types/models/account.ts +24 -0
- package/src/types/models/bank-sync.ts +23 -0
- package/src/types/models/bank.ts +6 -0
- package/src/types/models/category-group.ts +11 -0
- package/src/types/models/category.ts +13 -0
- package/src/types/models/dashboard.ts +199 -0
- package/src/types/models/gocardless.ts +84 -0
- package/src/types/models/import-transaction.ts +56 -0
- package/src/types/models/index.ts +23 -0
- package/src/types/models/nearby-payee.ts +7 -0
- package/src/types/models/note.ts +4 -0
- package/src/types/models/openid.ts +8 -0
- package/src/types/models/payee-location.ts +8 -0
- package/src/types/models/payee.ts +10 -0
- package/src/types/models/pluggyai.ts +19 -0
- package/src/types/models/reports.ts +144 -0
- package/src/types/models/rule.ts +174 -0
- package/src/types/models/schedule.ts +49 -0
- package/src/types/models/simplefin.ts +28 -0
- package/src/types/models/tags.ts +6 -0
- package/src/types/models/templates.ts +135 -0
- package/src/types/models/transaction-filter.ts +9 -0
- package/src/types/models/transaction.ts +39 -0
- package/src/types/models/user-access.ts +10 -0
- package/src/types/models/user.ts +25 -0
- package/src/types/prefs.ts +167 -0
- package/src/types/server-events.ts +86 -0
- package/src/types/server-handlers.ts +27 -0
- package/src/types/util.ts +26 -0
- package/tsconfig.json +34 -0
- package/typings/pegjs.ts +1 -0
- package/typings/process-worker.ts +12 -0
- package/typings/vite-plugin-peggy-loader.ts +1 -0
- package/typings/window.ts +62 -0
- package/vite.config.ts +109 -0
- package/vite.desktop.config.ts +59 -0
- package/vitest.config.ts +43 -0
- 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
|
+
});
|