json-fs-query 2.2.1 → 2.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.
@@ -1,2 +1,2 @@
1
- export declare const joinEntity: (entity: any, joinEntity: any, dataSets?: {}, joinClauses?: any[], entityAlreadyResolved?: boolean) => Promise<{}>;
1
+ export declare const joinEntity: (entity: any, joinEntity: any, dataSets?: {}, joinClauses?: any[], entityAlreadyResolved?: boolean, joinAlreadyResolved?: boolean) => Promise<{}>;
2
2
  //# sourceMappingURL=joinEntity.d.ts.map
@@ -8,7 +8,7 @@ var _siFunciona = _interopRequireDefault(require("si-funciona"));
8
8
  var _readEntity = require("./readEntity");
9
9
  var _useJoinClause = require("../utilities/useJoinClause");
10
10
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
- const joinEntity = async (entity, joinEntity, dataSets = {}, joinClauses = [], entityAlreadyResolved = false) => {
11
+ const joinEntity = async (entity, joinEntity, dataSets = {}, joinClauses = [], entityAlreadyResolved = false, joinAlreadyResolved = false) => {
12
12
  // Same ambiguity readEntity() always has (can't tell "never loaded" from "loaded, zero rows
13
13
  // matched") - runQuery already resolved entity's rows via an indexed condition in that second
14
14
  // case, so entityAlreadyResolved tells this call not to reload and silently discard that result.
@@ -18,7 +18,10 @@ const joinEntity = async (entity, joinEntity, dataSets = {}, joinClauses = [], e
18
18
  if (!joinClauses.length) {
19
19
  return dataSets;
20
20
  }
21
- dataSets = await (0, _readEntity.readEntity)(joinEntity, dataSets);
21
+ // The same for joinEntity: an indexed condition on its field may have resolved its rows to none
22
+ if (!joinAlreadyResolved) {
23
+ dataSets = await (0, _readEntity.readEntity)(joinEntity, dataSets);
24
+ }
22
25
  for (const anEntity in dataSets[entity]) {
23
26
  let filterJoins = _siFunciona.default.cloneObject(dataSets);
24
27
  filterJoins[entity] = [dataSets[entity][anEntity]];
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.joinEntity=void 0;var _siFunciona=_interopRequireDefault(require("si-funciona")),_readEntity=require("./readEntity"),_useJoinClause=require("../utilities/useJoinClause");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}const joinEntity=async(e,t,i={},n=[],r=!1)=>{if(r||(i=await(0,_readEntity.readEntity)(e,i)),!n.length)return i;i=await(0,_readEntity.readEntity)(t,i);for(const r in i[e]){let u=_siFunciona.default.cloneObject(i);u[e]=[i[e][r]];for(const e of n)u=await(0,_useJoinClause.useJoinClause)(e,i,u);i[e][r][t]=u[t]}return i};exports.joinEntity=joinEntity;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.joinEntity=void 0;var _siFunciona=_interopRequireDefault(require("si-funciona")),_readEntity=require("./readEntity"),_useJoinClause=require("../utilities/useJoinClause");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}const joinEntity=async(e,t,i={},n=[],r=!1,u=!1)=>{if(r||(i=await(0,_readEntity.readEntity)(e,i)),!n.length)return i;u||(i=await(0,_readEntity.readEntity)(t,i));for(const r in i[e]){let u=_siFunciona.default.cloneObject(i);u[e]=[i[e][r]];for(const e of n)u=await(0,_useJoinClause.useJoinClause)(e,i,u);i[e][r][t]=u[t]}return i};exports.joinEntity=joinEntity;
@@ -1,8 +1,39 @@
1
- export declare const where: (dataSet?: any[], { property, joinEntity, comparator, value, type }?: {
2
- property?: string;
3
- joinEntity?: any;
4
- comparator?: string;
5
- value?: any;
6
- type?: any;
7
- }) => any[];
1
+ import { ConditionNode } from '../utilities/conditionTree';
2
+ /**
3
+ * SQL's three-valued logic: a condition is true, false, or unknown (null here), see makeCondition.
4
+ */
5
+ export type Truth = boolean | null;
6
+ /**
7
+ * The rows for which one condition is true (unknown, from a null or missing field, isn't).
8
+ * @param dataSet
9
+ * @param condition
10
+ */
11
+ export declare const where: (dataSet?: any[], condition?: {}) => any[];
12
+ /**
13
+ * A where-clause tree on one row, with SQL's three-valued logic: unknown (null) stays unknown through `not`, and
14
+ * `and` / `or` decide on it only when another child settles the result.
15
+ *
16
+ * | | result |
17
+ * |---|---|
18
+ * | `not unknown` | unknown |
19
+ * | `false and unknown` | false |
20
+ * | `true and unknown` | unknown |
21
+ * | `true or unknown` | true |
22
+ * | `false or unknown` | unknown |
23
+ * @param data
24
+ * @param node
25
+ */
26
+ export declare const treeTruth: (data: any, node: ConditionNode) => Truth;
27
+ /**
28
+ * The rows that match a where-clause tree (see ConditionNode): only those for which the whole tree is true, so
29
+ * `not (n = 5)` returns the same rows as `n != 5` (neither returns a row where n is null or missing). Every row when
30
+ * there is no where clause.
31
+ *
32
+ * With a joinEntity (the tree uses its fields, see conditionEntities), it's SQL's inner join, evaluated per pair: each
33
+ * row keeps the joined rows for which the tree is true with that pair, and a row left with none is dropped.
34
+ * @param dataSet
35
+ * @param node
36
+ * @param joinEntity
37
+ */
38
+ export declare const whereTree: (dataSet?: any[], node?: ConditionNode | undefined, joinEntity?: string | undefined) => any[];
8
39
  //# sourceMappingURL=where.d.ts.map
@@ -3,12 +3,14 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.where = void 0;
6
+ exports.whereTree = exports.where = exports.treeTruth = void 0;
7
7
  require("core-js/modules/es.array.includes.js");
8
8
  require("core-js/modules/esnext.iterator.constructor.js");
9
9
  require("core-js/modules/esnext.iterator.filter.js");
10
+ require("core-js/modules/esnext.iterator.flat-map.js");
10
11
  require("core-js/modules/esnext.iterator.some.js");
11
12
  var _compareAs = require("../utilities/compareAs");
13
+ var _conditionTree = require("../utilities/conditionTree");
12
14
  const likeMatch = (dataValue, pattern) => {
13
15
  if (typeof dataValue !== 'string' || typeof pattern !== 'string') {
14
16
  return false;
@@ -21,8 +23,9 @@ const likeMatch = (dataValue, pattern) => {
21
23
  * '3' and 3 match in a string or int field. Ordering (>, >=, <, <=, between) compares by it too (see compareAs): a
22
24
  * string field orders as text ('10' < '9'), an int field as numbers. like compares the raw values.
23
25
  *
24
- * Null and missing are their own cases, as in SQL: a field that is null or missing matches no comparison (not even
25
- * `!=`), and a null never equals anything, not even another null. Only these test for them:
26
+ * Null and missing are their own cases, as in SQL: a comparison on a field that is null or missing is unknown (null),
27
+ * neither true nor false, so it matches nothing (not even `!=`) and `not` doesn't turn it into a match. A null never
28
+ * equals anything, not even another null. Only these test for them, and they are never unknown:
26
29
  * - `is null`: the field is there and holds null
27
30
  * - `is not null`: the field is there and holds a value (so a missing field doesn't match either)
28
31
  * - `exists` / `does not exist`: the field is there (null counts) / is missing
@@ -45,7 +48,7 @@ const makeCondition = (data, property, comparator, value, type = undefined) => {
45
48
  return exists && data[property] !== null;
46
49
  }
47
50
  if (!exists || data[property] === null || value === null) {
48
- return false;
51
+ return null;
49
52
  }
50
53
  const dataValue = data[property];
51
54
  const typedValue = (0, _compareAs.compareAs)(type, dataValue);
@@ -64,25 +67,128 @@ const makeCondition = (data, property, comparator, value, type = undefined) => {
64
67
  case '<=':
65
68
  return typedValue <= (0, _compareAs.compareAs)(type, value);
66
69
  case 'in':
67
- return Array.isArray(value) && (type ? value.some(item => (0, _compareAs.sameValue)(type, dataValue, item)) : value.includes(dataValue));
70
+ {
71
+ if (!Array.isArray(value)) {
72
+ return false;
73
+ }
74
+ if (type ? value.some(item => item !== null && (0, _compareAs.sameValue)(type, dataValue, item)) : value.includes(dataValue)) {
75
+ return true;
76
+ }
77
+ // SQL: `x in [1, null]` is x = 1 or x = null, so with no match it's unknown (not false), and not (...) keeps nothing
78
+ return value.includes(null) ? null : false;
79
+ }
68
80
  case 'between':
69
- // A null bound would act as 0 in >= / <=: it bounds nothing, so nothing matches (the parser refuses it)
70
- return Array.isArray(value) && value.length === 2 && !value.includes(null) && typedValue >= (0, _compareAs.compareAs)(type, value[0]) && typedValue <= (0, _compareAs.compareAs)(type, value[1]);
81
+ if (!Array.isArray(value) || value.length !== 2) {
82
+ return false;
83
+ }
84
+ // A null bound would act as 0 in >= / <=: it bounds nothing, so the result is unknown (the parser refuses it)
85
+ if (value.includes(null)) {
86
+ return null;
87
+ }
88
+ return typedValue >= (0, _compareAs.compareAs)(type, value[0]) && typedValue <= (0, _compareAs.compareAs)(type, value[1]);
71
89
  case 'like':
72
90
  return likeMatch(dataValue, value);
73
91
  }
74
92
  return false;
75
93
  };
76
- const where = (dataSet = [], {
94
+ /**
95
+ * One condition on one row: true, false or unknown (shared by where and whereTree).
96
+ * @param data
97
+ * @param condition
98
+ */
99
+ const conditionTruth = (data, {
77
100
  property = '',
78
101
  joinEntity = undefined,
79
102
  comparator = '=',
80
103
  value = null,
81
104
  type = undefined
82
- } = {}) => {
83
- return dataSet.filter(data => {
84
- const testProperty = joinEntity ? `${joinEntity}.${property}` : property;
85
- return makeCondition(data, testProperty, comparator, value, type);
105
+ } = {}) => makeCondition(data, joinEntity ? `${joinEntity}.${property}` : property, comparator, value, type);
106
+ /**
107
+ * The rows for which one condition is true (unknown, from a null or missing field, isn't).
108
+ * @param dataSet
109
+ * @param condition
110
+ */
111
+ const where = (dataSet = [], condition = {}) => dataSet.filter(data => conditionTruth(data, condition) === true);
112
+ /**
113
+ * A where-clause tree on one row, with SQL's three-valued logic: unknown (null) stays unknown through `not`, and
114
+ * `and` / `or` decide on it only when another child settles the result.
115
+ *
116
+ * | | result |
117
+ * |---|---|
118
+ * | `not unknown` | unknown |
119
+ * | `false and unknown` | false |
120
+ * | `true and unknown` | unknown |
121
+ * | `true or unknown` | true |
122
+ * | `false or unknown` | unknown |
123
+ * @param data
124
+ * @param node
125
+ */
126
+ exports.where = where;
127
+ const treeTruth = (data, node) => {
128
+ if ((0, _conditionTree.isCondition)(node)) {
129
+ return conditionTruth(data, node);
130
+ }
131
+ if ('not' in node) {
132
+ const truth = treeTruth(data, node.not);
133
+ return truth === null ? null : !truth;
134
+ }
135
+ // and: false wins, then unknown; or: true wins, then unknown
136
+ const settles = !('and' in node);
137
+ let result = !settles;
138
+ for (const child of 'and' in node ? node.and : node.or) {
139
+ const truth = treeTruth(data, child);
140
+ if (truth === settles) {
141
+ return settles;
142
+ }
143
+ if (truth === null) {
144
+ result = null;
145
+ }
146
+ }
147
+ return result;
148
+ };
149
+ /**
150
+ * One row seen together with one of its joined rows, for evaluating a where clause on the pair: the row's own fields,
151
+ * plus the joined row's as `joinEntity.field` (a field already on the row, such as a merged one, is kept).
152
+ * @param row
153
+ * @param joinEntity
154
+ * @param joinedRow
155
+ */
156
+ exports.treeTruth = treeTruth;
157
+ const pairView = (row, joinEntity, joinedRow) => {
158
+ const view = Object.assign({}, row);
159
+ for (const field of Object.keys(joinedRow)) {
160
+ // A merged field on the joined row is already named `entity.field`
161
+ const name = field.includes('.') ? field : `${joinEntity}.${field}`;
162
+ if (!Object.prototype.hasOwnProperty.call(view, name)) {
163
+ view[name] = joinedRow[field];
164
+ }
165
+ }
166
+ return view;
167
+ };
168
+ /**
169
+ * The rows that match a where-clause tree (see ConditionNode): only those for which the whole tree is true, so
170
+ * `not (n = 5)` returns the same rows as `n != 5` (neither returns a row where n is null or missing). Every row when
171
+ * there is no where clause.
172
+ *
173
+ * With a joinEntity (the tree uses its fields, see conditionEntities), it's SQL's inner join, evaluated per pair: each
174
+ * row keeps the joined rows for which the tree is true with that pair, and a row left with none is dropped.
175
+ * @param dataSet
176
+ * @param node
177
+ * @param joinEntity
178
+ */
179
+ const whereTree = (dataSet = [], node = undefined, joinEntity = undefined) => {
180
+ if (typeof node === 'undefined') {
181
+ return dataSet;
182
+ }
183
+ if (typeof joinEntity === 'undefined') {
184
+ return dataSet.filter(data => treeTruth(data, node) === true);
185
+ }
186
+ return dataSet.flatMap(data => {
187
+ const joined = Array.isArray(data[joinEntity]) ? data[joinEntity] : [];
188
+ const kept = joined.filter(joinedRow => treeTruth(pairView(data, joinEntity, joinedRow), node) === true);
189
+ return kept.length ? [Object.assign({}, data, {
190
+ [joinEntity]: kept
191
+ })] : [];
86
192
  });
87
193
  };
88
- exports.where = where;
194
+ exports.whereTree = whereTree;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.where=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.some.js");var _compareAs=require("../utilities/compareAs");const likeMatch=(e,r)=>{if("string"!=typeof e||"string"!=typeof r)return!1;const s=r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/%/g,".*").replace(/_/g,".");return new RegExp(`^${s}$`,"i").test(e)},makeCondition=(e,r,s,t,a=void 0)=>{const o=Object.prototype.hasOwnProperty.call(e,r);switch(s){case"exists":return o;case"does not exist":return!o;case"is null":return o&&null===e[r];case"is not null":return o&&null!==e[r]}if(!o||null===e[r]||null===t)return!1;const c=e[r],n=(0,_compareAs.compareAs)(a,c);switch(s){case"=":return(0,_compareAs.sameValue)(a,c,t);case"!=":case"<>":return!(0,_compareAs.sameValue)(a,c,t);case">":return n>(0,_compareAs.compareAs)(a,t);case">=":return n>=(0,_compareAs.compareAs)(a,t);case"<":return n<(0,_compareAs.compareAs)(a,t);case"<=":return n<=(0,_compareAs.compareAs)(a,t);case"in":return Array.isArray(t)&&(a?t.some((e=>(0,_compareAs.sameValue)(a,c,e))):t.includes(c));case"between":return Array.isArray(t)&&2===t.length&&!t.includes(null)&&n>=(0,_compareAs.compareAs)(a,t[0])&&n<=(0,_compareAs.compareAs)(a,t[1]);case"like":return likeMatch(c,t)}return!1},where=(e=[],{property:r="",joinEntity:s,comparator:t="=",value:a=null,type:o}={})=>e.filter((e=>makeCondition(e,s?`${s}.${r}`:r,t,a,o)));exports.where=where;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.whereTree=exports.where=exports.treeTruth=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.flat-map.js"),require("core-js/modules/esnext.iterator.some.js");var _compareAs=require("../utilities/compareAs"),_conditionTree=require("../utilities/conditionTree");const likeMatch=(e,r)=>{if("string"!=typeof e||"string"!=typeof r)return!1;const t=r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/%/g,".*").replace(/_/g,".");return new RegExp(`^${t}$`,"i").test(e)},makeCondition=(e,r,t,s,n=void 0)=>{const o=Object.prototype.hasOwnProperty.call(e,r);switch(t){case"exists":return o;case"does not exist":return!o;case"is null":return o&&null===e[r];case"is not null":return o&&null!==e[r]}if(!o||null===e[r]||null===s)return null;const i=e[r],c=(0,_compareAs.compareAs)(n,i);switch(t){case"=":return(0,_compareAs.sameValue)(n,i,s);case"!=":case"<>":return!(0,_compareAs.sameValue)(n,i,s);case">":return c>(0,_compareAs.compareAs)(n,s);case">=":return c>=(0,_compareAs.compareAs)(n,s);case"<":return c<(0,_compareAs.compareAs)(n,s);case"<=":return c<=(0,_compareAs.compareAs)(n,s);case"in":return!!Array.isArray(s)&&(!!(n?s.some((e=>null!==e&&(0,_compareAs.sameValue)(n,i,e))):s.includes(i))||!!s.includes(null)&&null);case"between":return!(!Array.isArray(s)||2!==s.length)&&(s.includes(null)?null:c>=(0,_compareAs.compareAs)(n,s[0])&&c<=(0,_compareAs.compareAs)(n,s[1]));case"like":return likeMatch(i,s)}return!1},conditionTruth=(e,{property:r="",joinEntity:t,comparator:s="=",value:n=null,type:o}={})=>makeCondition(e,t?`${t}.${r}`:r,s,n,o),where=(e=[],r={})=>e.filter((e=>!0===conditionTruth(e,r)));exports.where=where;const treeTruth=(e,r)=>{if((0,_conditionTree.isCondition)(r))return conditionTruth(e,r);if("not"in r){const t=treeTruth(e,r.not);return null===t?null:!t}const t=!("and"in r);let s=!t;for(const n of"and"in r?r.and:r.or){const r=treeTruth(e,n);if(r===t)return t;null===r&&(s=null)}return s};exports.treeTruth=treeTruth;const pairView=(e,r,t)=>{const s=Object.assign({},e);for(const e of Object.keys(t)){const n=e.includes(".")?e:`${r}.${e}`;Object.prototype.hasOwnProperty.call(s,n)||(s[n]=t[e])}return s},whereTree=(e=[],r=void 0,t=void 0)=>void 0===r?e:void 0===t?e.filter((e=>!0===treeTruth(e,r))):e.flatMap((e=>{const s=(Array.isArray(e[t])?e[t]:[]).filter((s=>!0===treeTruth(pairView(e,t,s),r)));return s.length?[Object.assign({},e,{[t]:s})]:[]}));exports.whereTree=whereTree;
@@ -0,0 +1,20 @@
1
+ import { ParsedQuery } from './parsers/parseQuery';
2
+ /**
3
+ * Check which entity each condition's field is on, before anything is read, and say whether the where clause has to be
4
+ * evaluated per pair of a row and one of its joined rows:
5
+ * - the queried entity (`workouts.date`): written as the plain field (`date`), which is where the row holds it
6
+ * - the joined entity (`read workouts.exercises on ...`, `exercises.name`): per pair (see whereTree), so returns true
7
+ * - an entity merged into the rows or the joined rows (`merge workouts.exercise_id with exercises._id`): read from the
8
+ * merged fields (`exercises.name` on the row), as before
9
+ * - anything else is an error: an entity that only links the join (`exercise_muscle` in `workouts.muscles`), or one
10
+ * that isn't in the query at all (these used to match nothing, or fail to read the entity)
11
+ *
12
+ * update and delete apply their conditions before any join or merge runs, so they only take the entity's own fields.
13
+ * (An indexed joined-field condition used to be dropped once its index had narrowed the joined rows, so the command hit
14
+ * every row the other conditions allowed: `delete workouts.exercises on ... where exercises._id = 1` deleted every
15
+ * workout. Refused since #79.)
16
+ * @param parsed - its conditions on the queried entity lose their `entity.` prefix
17
+ * @throws {Error} For a condition on a field the query can't check
18
+ */
19
+ export declare const conditionEntities: (parsed: ParsedQuery) => boolean;
20
+ //# sourceMappingURL=conditionEntities.d.ts.map
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.conditionEntities = void 0;
7
+ require("core-js/modules/esnext.iterator.constructor.js");
8
+ require("core-js/modules/esnext.iterator.filter.js");
9
+ require("core-js/modules/esnext.iterator.flat-map.js");
10
+ require("core-js/modules/esnext.iterator.map.js");
11
+ require("core-js/modules/esnext.set.add-all.js");
12
+ require("core-js/modules/esnext.set.delete-all.js");
13
+ require("core-js/modules/esnext.set.difference.js");
14
+ require("core-js/modules/esnext.set.every.js");
15
+ require("core-js/modules/esnext.set.filter.js");
16
+ require("core-js/modules/esnext.set.find.js");
17
+ require("core-js/modules/esnext.set.intersection.js");
18
+ require("core-js/modules/esnext.set.is-disjoint-from.js");
19
+ require("core-js/modules/esnext.set.is-subset-of.js");
20
+ require("core-js/modules/esnext.set.is-superset-of.js");
21
+ require("core-js/modules/esnext.set.join.js");
22
+ require("core-js/modules/esnext.set.map.js");
23
+ require("core-js/modules/esnext.set.reduce.js");
24
+ require("core-js/modules/esnext.set.some.js");
25
+ require("core-js/modules/esnext.set.symmetric-difference.js");
26
+ require("core-js/modules/esnext.set.union.js");
27
+ var _isEntityProperty = require("./parsers/isEntityProperty");
28
+ var _splitEntityProperty = require("./parsers/splitEntityProperty");
29
+ var _conditionTree = require("./conditionTree");
30
+ /**
31
+ * Check which entity each condition's field is on, before anything is read, and say whether the where clause has to be
32
+ * evaluated per pair of a row and one of its joined rows:
33
+ * - the queried entity (`workouts.date`): written as the plain field (`date`), which is where the row holds it
34
+ * - the joined entity (`read workouts.exercises on ...`, `exercises.name`): per pair (see whereTree), so returns true
35
+ * - an entity merged into the rows or the joined rows (`merge workouts.exercise_id with exercises._id`): read from the
36
+ * merged fields (`exercises.name` on the row), as before
37
+ * - anything else is an error: an entity that only links the join (`exercise_muscle` in `workouts.muscles`), or one
38
+ * that isn't in the query at all (these used to match nothing, or fail to read the entity)
39
+ *
40
+ * update and delete apply their conditions before any join or merge runs, so they only take the entity's own fields.
41
+ * (An indexed joined-field condition used to be dropped once its index had narrowed the joined rows, so the command hit
42
+ * every row the other conditions allowed: `delete workouts.exercises on ... where exercises._id = 1` deleted every
43
+ * workout. Refused since #79.)
44
+ * @param parsed - its conditions on the queried entity lose their `entity.` prefix
45
+ * @throws {Error} For a condition on a field the query can't check
46
+ */
47
+ const conditionEntities = parsed => {
48
+ const entity = parsed.entity;
49
+ const joinEntity = parsed.joinClauses.length ? parsed.joinEntity : undefined;
50
+ const joined = new Set([entity, joinEntity]);
51
+ const merged = new Set(parsed.mergeJoins.filter(merge => joined.has((0, _splitEntityProperty.splitEntityProperty)(merge.propertyA).entity)).map(merge => (0, _splitEntityProperty.splitEntityProperty)(merge.propertyB).entity));
52
+ const linking = new Set(parsed.joinClauses.flatMap(clause => [clause.propertyA, clause.propertyB].map(property => (0, _splitEntityProperty.splitEntityProperty)(property).entity)).filter(name => !joined.has(name) && !merged.has(name)));
53
+ const changes = parsed.command === 'update' || parsed.command === 'delete';
54
+ let perPair = false;
55
+ for (const condition of (0, _conditionTree.conditionsOf)(parsed.where)) {
56
+ if (!(0, _isEntityProperty.isEntityProperty)(condition.property)) {
57
+ continue;
58
+ }
59
+ const {
60
+ entity: fieldEntity,
61
+ property
62
+ } = (0, _splitEntityProperty.splitEntityProperty)(condition.property);
63
+ if (fieldEntity === entity) {
64
+ condition.property = property;
65
+ continue;
66
+ }
67
+ if (changes) {
68
+ throw new Error(`${parsed.command} can only use conditions on ${entity}'s own fields, not ${condition.property}`);
69
+ }
70
+ if (fieldEntity === joinEntity) {
71
+ perPair = true;
72
+ continue;
73
+ }
74
+ if (merged.has(fieldEntity)) {
75
+ continue;
76
+ }
77
+ if (linking.has(fieldEntity)) {
78
+ throw new Error(`${fieldEntity} only links ${entity} to ${joinEntity}: a condition can't use its fields (${condition.property})`);
79
+ }
80
+ const usable = [entity, joinEntity, ...merged].filter(name => typeof name !== 'undefined').join(', ');
81
+ throw new Error(`${fieldEntity} is not part of this query (${condition.property}): conditions can use the fields of ${usable}`);
82
+ }
83
+ return perPair;
84
+ };
85
+ exports.conditionEntities = conditionEntities;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.conditionEntities=void 0,require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.flat-map.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.set.add-all.js"),require("core-js/modules/esnext.set.delete-all.js"),require("core-js/modules/esnext.set.difference.js"),require("core-js/modules/esnext.set.every.js"),require("core-js/modules/esnext.set.filter.js"),require("core-js/modules/esnext.set.find.js"),require("core-js/modules/esnext.set.intersection.js"),require("core-js/modules/esnext.set.is-disjoint-from.js"),require("core-js/modules/esnext.set.is-subset-of.js"),require("core-js/modules/esnext.set.is-superset-of.js"),require("core-js/modules/esnext.set.join.js"),require("core-js/modules/esnext.set.map.js"),require("core-js/modules/esnext.set.reduce.js"),require("core-js/modules/esnext.set.some.js"),require("core-js/modules/esnext.set.symmetric-difference.js"),require("core-js/modules/esnext.set.union.js");var _isEntityProperty=require("./parsers/isEntityProperty"),_splitEntityProperty=require("./parsers/splitEntityProperty"),_conditionTree=require("./conditionTree");const conditionEntities=e=>{const t=e.entity,r=e.joinClauses.length?e.joinEntity:void 0,s=new Set([t,r]),o=new Set(e.mergeJoins.filter((e=>s.has((0,_splitEntityProperty.splitEntityProperty)(e.propertyA).entity))).map((e=>(0,_splitEntityProperty.splitEntityProperty)(e.propertyB).entity))),i=new Set(e.joinClauses.flatMap((e=>[e.propertyA,e.propertyB].map((e=>(0,_splitEntityProperty.splitEntityProperty)(e).entity)))).filter((e=>!s.has(e)&&!o.has(e)))),n="update"===e.command||"delete"===e.command;let u=!1;for(const s of(0,_conditionTree.conditionsOf)(e.where)){if(!(0,_isEntityProperty.isEntityProperty)(s.property))continue;const{entity:p,property:c}=(0,_splitEntityProperty.splitEntityProperty)(s.property);if(p===t){s.property=c;continue}if(n)throw new Error(`${e.command} can only use conditions on ${t}'s own fields, not ${s.property}`);if(p===r){u=!0;continue}if(o.has(p))continue;if(i.has(p))throw new Error(`${p} only links ${t} to ${r}: a condition can't use its fields (${s.property})`);const l=[t,r,...o].filter((e=>void 0!==e)).join(", ");throw new Error(`${p} is not part of this query (${s.property}): conditions can use the fields of ${l}`)}return u};exports.conditionEntities=conditionEntities;
@@ -0,0 +1,36 @@
1
+ import type { ParsedCondition } from './parsers/findConditions';
2
+ /**
3
+ * A where clause as a tree: a condition, or `and` / `or` over several nodes, or `not` over one. `a and b and c` is one
4
+ * `and` node with three children, not nested pairs.
5
+ */
6
+ export type ConditionNode = ParsedCondition | {
7
+ and: ConditionNode[];
8
+ } | {
9
+ or: ConditionNode[];
10
+ } | {
11
+ not: ConditionNode;
12
+ };
13
+ /**
14
+ * Whether a node is a single condition rather than `and` / `or` / `not`.
15
+ * @param node
16
+ */
17
+ export declare const isCondition: (node: ConditionNode) => node is ParsedCondition;
18
+ /**
19
+ * The tree for conditions that must all match: none gives undefined (no where clause), one gives that condition itself,
20
+ * several give one `and` node.
21
+ * @param conditions
22
+ */
23
+ export declare const allOf: (conditions: ConditionNode[]) => ConditionNode | undefined;
24
+ /**
25
+ * Every condition in the tree, in order, whatever group it is in.
26
+ * @param node
27
+ */
28
+ export declare const conditionsOf: (node: ConditionNode | undefined) => ParsedCondition[];
29
+ /**
30
+ * The conditions every matching row must meet on their own: the tree itself when it is one condition, or the
31
+ * conditions directly under its top-level `and`. Only these may narrow the rows read through an index (an `or` or a
32
+ * `not` doesn't require any one of its conditions).
33
+ * @param node
34
+ */
35
+ export declare const requiredConditions: (node: ConditionNode | undefined) => ParsedCondition[];
36
+ //# sourceMappingURL=conditionTree.d.ts.map
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.requiredConditions = exports.isCondition = exports.conditionsOf = exports.allOf = void 0;
7
+ require("core-js/modules/esnext.iterator.constructor.js");
8
+ require("core-js/modules/esnext.iterator.filter.js");
9
+ require("core-js/modules/esnext.iterator.flat-map.js");
10
+ /**
11
+ * Whether a node is a single condition rather than `and` / `or` / `not`.
12
+ * @param node
13
+ */
14
+ const isCondition = node => !('and' in node) && !('or' in node) && !('not' in node);
15
+ /**
16
+ * The tree for conditions that must all match: none gives undefined (no where clause), one gives that condition itself,
17
+ * several give one `and` node.
18
+ * @param conditions
19
+ */
20
+ exports.isCondition = isCondition;
21
+ const allOf = conditions => {
22
+ if (!conditions.length) {
23
+ return undefined;
24
+ }
25
+ return conditions.length === 1 ? conditions[0] : {
26
+ and: conditions
27
+ };
28
+ };
29
+ /**
30
+ * Every condition in the tree, in order, whatever group it is in.
31
+ * @param node
32
+ */
33
+ exports.allOf = allOf;
34
+ const conditionsOf = node => {
35
+ if (typeof node === 'undefined') {
36
+ return [];
37
+ }
38
+ if (isCondition(node)) {
39
+ return [node];
40
+ }
41
+ if ('not' in node) {
42
+ return conditionsOf(node.not);
43
+ }
44
+ return ('and' in node ? node.and : node.or).flatMap(conditionsOf);
45
+ };
46
+ /**
47
+ * The conditions every matching row must meet on their own: the tree itself when it is one condition, or the
48
+ * conditions directly under its top-level `and`. Only these may narrow the rows read through an index (an `or` or a
49
+ * `not` doesn't require any one of its conditions).
50
+ * @param node
51
+ */
52
+ exports.conditionsOf = conditionsOf;
53
+ const requiredConditions = node => {
54
+ if (typeof node === 'undefined') {
55
+ return [];
56
+ }
57
+ if (isCondition(node)) {
58
+ return [node];
59
+ }
60
+ return 'and' in node ? node.and.filter(isCondition) : [];
61
+ };
62
+ exports.requiredConditions = requiredConditions;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.requiredConditions=exports.isCondition=exports.conditionsOf=exports.allOf=void 0,require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.flat-map.js");const isCondition=o=>!("and"in o)&&!("or"in o)&&!("not"in o);exports.isCondition=isCondition;const allOf=o=>{if(o.length)return 1===o.length?o[0]:{and:o}};exports.allOf=allOf;const conditionsOf=o=>void 0===o?[]:isCondition(o)?[o]:"not"in o?conditionsOf(o.not):("and"in o?o.and:o.or).flatMap(conditionsOf);exports.conditionsOf=conditionsOf;const requiredConditions=o=>void 0===o?[]:isCondition(o)?[o]:"and"in o?o.and.filter(isCondition):[];exports.requiredConditions=requiredConditions;
@@ -1,15 +1,18 @@
1
1
  import { ParsedQuery } from './parsers/parseQuery';
2
2
  /**
3
- * Load only the rows the indexed conditions match, and leave the other conditions for where() to apply later.
4
- * Conditions are joined by `and`, so a row is loaded only when every indexed condition on its entity matched it: each
5
- * condition narrows the files the ones before it matched. (Each indexed condition used to replace the rows of the one
6
- * before, so `where date = 'x' and where exercise_id = 1` returned every row with exercise_id 1, whatever its date.)
3
+ * Load only the candidate rows the indexes allow for the where clause (see candidatesOf): an index only narrows which
4
+ * rows are read. Every condition, indexed or not, stays in parsed.where and is still applied to the candidates
5
+ * afterwards (runQuery), so an index can make a query faster but never changes its result (where-clauses decision 3;
6
+ * it used to remove the conditions it answered, and the 3 bugs fixed in #69 came from that split).
7
+ *
8
+ * An indexed condition on a joined entity's field (`exercises._id = 1`) narrows that entity's rows only when every
9
+ * row must meet it (directly under the top-level `and`, see requiredConditions). It stays in the tree too: the where
10
+ * clause is then evaluated per pair of a row and a joined row (see whereTree).
7
11
  * @param parsed
8
12
  * @param recordData
9
13
  * @param dataSet
10
- * @param resolved - Filled with each entity whose rows were loaded here: its rows in dataSet are final (possibly none)
11
- * and must not be reloaded, even when other conditions are left for where() (an empty list otherwise reads as "not
12
- * loaded yet", see readEntity).
14
+ * @param resolved - Filled with each entity whose candidate rows were loaded here: they must not be reloaded (an
15
+ * empty list otherwise reads as "not loaded yet", see readEntity).
13
16
  */
14
17
  export declare const getConditionIndexedList: (parsed: ParsedQuery, recordData: any, dataSet: any, resolved?: Set<string>) => Promise<ParsedQuery>;
15
18
  //# sourceMappingURL=getConditionIndexedList.d.ts.map