@naturalcycles/db-lib 8.46.1 → 8.47.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.
@@ -3,7 +3,7 @@ import { Readable } from 'node:stream';
3
3
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
4
4
  import { BaseCommonDB } from '../../base.common.db';
5
5
  import { CommonDB } from '../../common.db';
6
- import { CommonDBOptions, RunQueryResult } from '../../db.model';
6
+ import { CommonDBOptions, DBPatch, RunQueryResult } from '../../db.model';
7
7
  import { DBQuery } from '../../query/dbQuery';
8
8
  import { DBTransaction } from '../../transaction/dbTransaction';
9
9
  import { CacheDBCfg, CacheDBCreateOptions, CacheDBOptions, CacheDBSaveOptions, CacheDBStreamOptions } from './cache.db.model';
@@ -30,5 +30,6 @@ export declare class CacheDB extends BaseCommonDB implements CommonDB {
30
30
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBOptions): Promise<number>;
31
31
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBStreamOptions): Readable;
32
32
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBOptions): Promise<number>;
33
+ updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>, opt?: CacheDBOptions): Promise<number>;
33
34
  commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void>;
34
35
  }
@@ -195,6 +195,18 @@ class CacheDB extends base_common_db_1.BaseCommonDB {
195
195
  }
196
196
  return deletedIds;
197
197
  }
198
+ async updateByQuery(q, patch, opt = {}) {
199
+ let updated;
200
+ if (!opt.onlyCache && !this.cfg.onlyCache) {
201
+ updated = await this.cfg.downstreamDB.updateByQuery(q, patch, opt);
202
+ }
203
+ if (!opt.skipCache && !this.cfg.skipCache) {
204
+ const cacheResult = this.cfg.cacheDB.updateByQuery(q, patch, opt);
205
+ if (this.cfg.awaitCache)
206
+ updated ??= await cacheResult;
207
+ }
208
+ return updated || 0;
209
+ }
198
210
  async commitTransaction(tx, opt) {
199
211
  await this.cfg.downstreamDB.commitTransaction(tx, opt);
200
212
  await this.cfg.cacheDB.commitTransaction(tx, opt);
@@ -1,6 +1,6 @@
1
1
  import { JsonSchemaObject, StringMap, JsonSchemaRootObject, ObjectWithId, CommonLogger } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { CommonDB, DBTransaction } from '../..';
3
+ import { CommonDB, DBPatch, DBTransaction } from '../..';
4
4
  import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, RunQueryResult } from '../../db.model';
5
5
  import { DBQuery } from '../../query/dbQuery';
6
6
  export interface InMemoryDBCfg {
@@ -56,6 +56,7 @@ export declare class InMemoryDB implements CommonDB {
56
56
  saveBatch<ROW extends Partial<ObjectWithId>>(_table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
57
57
  deleteByIds<ROW extends ObjectWithId>(_table: string, ids: ROW['id'][], _opt?: CommonDBOptions): Promise<number>;
58
58
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
59
+ updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>): Promise<number>;
59
60
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
60
61
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
61
62
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): ReadableTyped<ROW>;
@@ -94,14 +94,14 @@ class InMemoryDB {
94
94
  async deleteByIds(_table, ids, _opt) {
95
95
  const table = this.cfg.tablesPrefix + _table;
96
96
  this.data[table] ||= {};
97
- return ids
98
- .map(id => {
99
- const exists = !!this.data[table][id];
97
+ let count = 0;
98
+ ids.forEach(id => {
99
+ if (!this.data[table][id])
100
+ return;
100
101
  delete this.data[table][id];
101
- if (exists)
102
- return id;
103
- })
104
- .filter(Boolean).length;
102
+ count++;
103
+ });
104
+ return count;
105
105
  }
106
106
  async deleteByQuery(q, _opt) {
107
107
  const table = this.cfg.tablesPrefix + q.table;
@@ -109,6 +109,24 @@ class InMemoryDB {
109
109
  const ids = rows.map(r => r.id);
110
110
  return await this.deleteByIds(q.table, ids);
111
111
  }
112
+ async updateByQuery(q, patch) {
113
+ const patchEntries = Object.entries(patch);
114
+ if (!patchEntries.length)
115
+ return 0;
116
+ const table = this.cfg.tablesPrefix + q.table;
117
+ const rows = (0, __1.queryInMemory)(q, Object.values(this.data[table] || {}));
118
+ rows.forEach((row) => {
119
+ patchEntries.forEach(([k, v]) => {
120
+ if (v instanceof __1.DBIncrement) {
121
+ row[k] = (row[k] || 0) + v.amount;
122
+ }
123
+ else {
124
+ row[k] = v;
125
+ }
126
+ });
127
+ });
128
+ return rows.length;
129
+ }
112
130
  async runQuery(q, _opt) {
113
131
  const table = this.cfg.tablesPrefix + q.table;
114
132
  return { rows: (0, __1.queryInMemory)(q, Object.values(this.data[table] || {})) };
@@ -147,9 +165,7 @@ class InMemoryDB {
147
165
  * Flushes all tables (all namespaces) at once.
148
166
  */
149
167
  async flushToDisk() {
150
- if (!this.cfg.persistenceEnabled) {
151
- throw new Error('flushToDisk() called but persistenceEnabled=false');
152
- }
168
+ (0, js_lib_1._assert)(this.cfg.persistenceEnabled, 'flushToDisk() called but persistenceEnabled=false');
153
169
  const { persistentStoragePath, persistZip } = this.cfg;
154
170
  const started = Date.now();
155
171
  await fs.emptyDir(persistentStoragePath);
@@ -175,9 +191,7 @@ class InMemoryDB {
175
191
  * Restores all tables (all namespaces) at once.
176
192
  */
177
193
  async restoreFromDisk() {
178
- if (!this.cfg.persistentStoragePath) {
179
- throw new Error('restoreFromDisk() called but persistenceEnabled=false');
180
- }
194
+ (0, js_lib_1._assert)(this.cfg.persistenceEnabled, 'restoreFromDisk() called but persistenceEnabled=false');
181
195
  const { persistentStoragePath } = this.cfg;
182
196
  const started = Date.now();
183
197
  await fs.ensureDir(persistentStoragePath);
@@ -17,6 +17,10 @@ const FILTER_FNS = {
17
17
  // Important: q.table is not used in this function, so tablesPrefix is not needed.
18
18
  // But should be careful here..
19
19
  function queryInMemory(q, rows = []) {
20
+ // .ids
21
+ if (q._ids?.length) {
22
+ rows = rows.filter(r => q._ids.includes(r.id));
23
+ }
20
24
  // .filter
21
25
  // eslint-disable-next-line unicorn/no-array-reduce
22
26
  rows = q._filters.reduce((rows, filter) => {
@@ -1,7 +1,7 @@
1
1
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
3
  import { CommonDB } from './common.db';
4
- import { CommonDBOptions, CommonDBSaveOptions, RunQueryResult } from './db.model';
4
+ import { CommonDBOptions, CommonDBSaveOptions, DBPatch, RunQueryResult } from './db.model';
5
5
  import { DBQuery } from './query/dbQuery';
6
6
  import { DBTransaction } from './transaction/dbTransaction';
7
7
  /**
@@ -12,14 +12,15 @@ export declare class BaseCommonDB implements CommonDB {
12
12
  ping(): Promise<void>;
13
13
  getTables(): Promise<string[]>;
14
14
  getTableSchema<ROW extends ObjectWithId>(table: string): Promise<JsonSchemaRootObject<ROW>>;
15
- createTable<ROW extends ObjectWithId>(_table: string, _schema: JsonSchemaObject<ROW>): Promise<void>;
16
- deleteByIds<ROW extends ObjectWithId>(_table: string, _ids: ROW['id'][]): Promise<number>;
17
- deleteByQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<number>;
18
- getByIds<ROW extends ObjectWithId>(_table: string, _ids: ROW['id'][]): Promise<ROW[]>;
19
- runQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<RunQueryResult<ROW>>;
20
- runQueryCount<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<number>;
21
- saveBatch<ROW extends Partial<ObjectWithId>>(_table: string, _rows: ROW[], _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
22
- streamQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): ReadableTyped<ROW>;
15
+ createTable<ROW extends ObjectWithId>(table: string, schema: JsonSchemaObject<ROW>): Promise<void>;
16
+ deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<number>;
17
+ deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number>;
18
+ updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>, opt?: CommonDBOptions): Promise<number>;
19
+ getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<ROW[]>;
20
+ runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<RunQueryResult<ROW>>;
21
+ runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number>;
22
+ saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
23
+ streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): ReadableTyped<ROW>;
23
24
  /**
24
25
  * Naive implementation.
25
26
  * Doesn't support rollback on error, hence doesn't pass dbTest.
@@ -1,45 +1,47 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseCommonDB = void 0;
4
- const node_stream_1 = require("node:stream");
5
- const dbTransaction_util_1 = require("./transaction/dbTransaction.util");
4
+ /* eslint-disable unused-imports/no-unused-vars */
6
5
  /**
7
6
  * No-op implementation of CommonDB interface.
8
7
  * To be extended by actual implementations.
9
8
  */
10
9
  class BaseCommonDB {
11
- async ping() { }
10
+ async ping() {
11
+ throw new Error('ping is not implemented');
12
+ }
12
13
  async getTables() {
13
- return [];
14
+ throw new Error('getTables is not implemented');
14
15
  }
15
16
  async getTableSchema(table) {
16
- return {
17
- $id: `${table}.schema.json`,
18
- type: 'object',
19
- additionalProperties: true,
20
- properties: {},
21
- required: [],
22
- };
17
+ throw new Error('getTableSchema is not implemented');
18
+ }
19
+ async createTable(table, schema) {
20
+ throw new Error('createTable is not implemented');
21
+ }
22
+ async deleteByIds(table, ids) {
23
+ throw new Error('deleteByIds is not implemented');
24
+ }
25
+ async deleteByQuery(q) {
26
+ throw new Error('deleteByQuery is not implemented');
23
27
  }
24
- async createTable(_table, _schema) { }
25
- async deleteByIds(_table, _ids) {
26
- return 0;
28
+ async updateByQuery(q, patch, opt) {
29
+ throw new Error('updateByQuery is not implemented');
27
30
  }
28
- async deleteByQuery(_q) {
29
- return 0;
31
+ async getByIds(table, ids) {
32
+ throw new Error('getByIds is not implemented');
30
33
  }
31
- async getByIds(_table, _ids) {
32
- return [];
34
+ async runQuery(q) {
35
+ throw new Error('runQuery is not implemented');
33
36
  }
34
- async runQuery(_q) {
35
- return { rows: [] };
37
+ async runQueryCount(q) {
38
+ throw new Error('runQueryCount is not implemented');
36
39
  }
37
- async runQueryCount(_q) {
38
- return 0;
40
+ async saveBatch(table, rows, opt) {
41
+ throw new Error('saveBatch is not implemented');
39
42
  }
40
- async saveBatch(_table, _rows, _opt) { }
41
- streamQuery(_q) {
42
- return node_stream_1.Readable.from([]);
43
+ streamQuery(q) {
44
+ throw new Error('streamQuery is not implemented');
43
45
  }
44
46
  /**
45
47
  * Naive implementation.
@@ -47,7 +49,7 @@ class BaseCommonDB {
47
49
  * To be extended.
48
50
  */
49
51
  async commitTransaction(tx, opt) {
50
- await (0, dbTransaction_util_1.commitDBTransactionSimple)(this, tx, opt);
52
+ throw new Error('commitTransaction is not implemented');
51
53
  }
52
54
  }
53
55
  exports.BaseCommonDB = BaseCommonDB;
@@ -1,6 +1,6 @@
1
1
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, RunQueryResult } from './db.model';
3
+ import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBPatch, RunQueryResult } from './db.model';
4
4
  import { DBQuery } from './query/dbQuery';
5
5
  import { DBTransaction } from './transaction/dbTransaction';
6
6
  export interface CommonDB {
@@ -48,6 +48,25 @@ export interface CommonDB {
48
48
  */
49
49
  deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], opt?: CommonDBOptions): Promise<number>;
50
50
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>;
51
+ /**
52
+ * Applies patch to the rows returned by the query.
53
+ *
54
+ * Example:
55
+ *
56
+ * UPDATE table SET A = B where $QUERY_CONDITION
57
+ *
58
+ * patch would be { A: 'B' } for that query.
59
+ *
60
+ * Supports "increment query", example:
61
+ *
62
+ * UPDATE table SET A = A + 1
63
+ *
64
+ * In that case patch would look like:
65
+ * { A: DBIncrement(1) }
66
+ *
67
+ * Returns number of rows affected.
68
+ */
69
+ updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>, opt?: CommonDBOptions): Promise<number>;
51
70
  /**
52
71
  * Should be implemented as a Transaction (best effort), which means that
53
72
  * either ALL or NONE of the operations should be applied.
@@ -1,6 +1,6 @@
1
1
  import { AnyObject, AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Saved, Unsaved } from '@naturalcycles/js-lib';
2
2
  import { AjvSchema, ObjectSchemaTyped, ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { DBDeleteByIdsOperation, DBModelType, DBOperation, DBSaveBatchOperation, RunQueryResult } from '../db.model';
3
+ import { DBDeleteByIdsOperation, DBModelType, DBOperation, DBPatch, DBSaveBatchOperation, RunQueryResult } from '../db.model';
4
4
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery';
5
5
  import { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoSaveOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions } from './common.dao.model';
6
6
  /**
@@ -120,6 +120,9 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
120
120
  deleteByQuery(q: DBQuery<DBM>, opt?: CommonDaoStreamForEachOptions<DBM> & {
121
121
  stream?: boolean;
122
122
  }): Promise<number>;
123
+ updateById(id: ID, patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
124
+ updateByIds(ids: ID[], patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
125
+ updateByQuery(q: DBQuery<DBM>, patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
123
126
  dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): Promise<undefined>;
124
127
  dbmToBM(_dbm?: DBM, opt?: CommonDaoOptions): Promise<Saved<BM>>;
125
128
  dbmsToBM(dbms: DBM[], opt?: CommonDaoOptions): Promise<Saved<BM>[]>;
@@ -711,6 +711,22 @@ class CommonDao {
711
711
  this.logSaveResult(started, op, q.table);
712
712
  return deleted;
713
713
  }
714
+ async updateById(id, patch, opt = {}) {
715
+ return await this.updateByQuery(this.query().byId(id), patch, opt);
716
+ }
717
+ async updateByIds(ids, patch, opt = {}) {
718
+ return await this.updateByQuery(this.query().byIds(ids), patch, opt);
719
+ }
720
+ async updateByQuery(q, patch, opt = {}) {
721
+ this.requireWriteAccess();
722
+ this.requireObjectMutability(opt);
723
+ q.table = opt.table || q.table;
724
+ const op = `updateByQuery(${q.pretty()})`;
725
+ const started = this.logStarted(op, q.table);
726
+ const updated = await this.cfg.db.updateByQuery(q, patch, opt);
727
+ this.logSaveResult(started, op, q.table);
728
+ return updated;
729
+ }
714
730
  async dbmToBM(_dbm, opt = {}) {
715
731
  if (!_dbm)
716
732
  return;
@@ -62,3 +62,16 @@ export declare enum DBModelType {
62
62
  BM = "BM",
63
63
  TM = "TM"
64
64
  }
65
+ /**
66
+ * Allows to construct a query similar to:
67
+ *
68
+ * UPDATE table SET A = A + 1
69
+ *
70
+ * In this case DBIncement.of(1) will be needed.
71
+ */
72
+ export declare class DBIncrement {
73
+ amount: number;
74
+ private constructor();
75
+ static of(amount: number): DBIncrement;
76
+ }
77
+ export type DBPatch<ROW extends Partial<ObjectWithId>> = Partial<Record<keyof ROW, ROW[keyof ROW] | DBIncrement>>;
package/dist/db.model.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DBModelType = exports.DBRelation = void 0;
3
+ exports.DBIncrement = exports.DBModelType = exports.DBRelation = void 0;
4
4
  var DBRelation;
5
5
  (function (DBRelation) {
6
6
  DBRelation["ONE_TO_ONE"] = "ONE_TO_ONE";
@@ -12,3 +12,19 @@ var DBModelType;
12
12
  DBModelType["BM"] = "BM";
13
13
  DBModelType["TM"] = "TM";
14
14
  })(DBModelType = exports.DBModelType || (exports.DBModelType = {}));
15
+ /**
16
+ * Allows to construct a query similar to:
17
+ *
18
+ * UPDATE table SET A = A + 1
19
+ *
20
+ * In this case DBIncement.of(1) will be needed.
21
+ */
22
+ class DBIncrement {
23
+ constructor(amount) {
24
+ this.amount = amount;
25
+ }
26
+ static of(amount) {
27
+ return new DBIncrement(amount);
28
+ }
29
+ }
30
+ exports.DBIncrement = DBIncrement;
@@ -1,6 +1,6 @@
1
1
  import { AnyObjectWithId, ObjectWithId, AsyncMapper, Saved, AnyObject } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { CommonDaoOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions } from '..';
3
+ import { CommonDaoOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, DBPatch } from '..';
4
4
  import { CommonDao } from '../commondao/common.dao';
5
5
  import { RunQueryResult } from '../db.model';
6
6
  /**
@@ -66,6 +66,7 @@ export declare class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
66
66
  _selectedFieldNames?: (keyof ROW)[];
67
67
  _groupByFieldNames?: (keyof ROW)[];
68
68
  _distinct: boolean;
69
+ _ids?: ROW['id'][];
69
70
  filter(name: keyof ROW, op: DBQueryFilterOperator, val: any): this;
70
71
  filterEq(name: keyof ROW, val: any): this;
71
72
  limit(limit: number): this;
@@ -76,6 +77,20 @@ export declare class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
76
77
  distinct(distinct?: boolean): this;
77
78
  startCursor(startCursor?: string): this;
78
79
  endCursor(endCursor?: string): this;
80
+ /**
81
+ * Allows to query by ids (one or many).
82
+ * Similar to:
83
+ * SELECT * FROM table where id in (a, b, c)
84
+ * or (if only 1 id is passed)
85
+ * SELECT * FROM table where id = a
86
+ */
87
+ byIds(ids: ROW['id'][]): this;
88
+ /**
89
+ * Allows to query by id.
90
+ * Similar to:
91
+ * SELECT * FROM table where id = a
92
+ */
93
+ byId(id: ROW['id']): this;
79
94
  clone(): DBQuery<ROW>;
80
95
  pretty(): string;
81
96
  prettyConditions(): string[];
@@ -97,6 +112,7 @@ export declare class RunnableDBQuery<BM extends Partial<ObjectWithId<ID>>, DBM e
97
112
  runQueryExtendedAsDBM(opt?: CommonDaoOptions): Promise<RunQueryResult<DBM>>;
98
113
  runQueryExtendedAsTM(opt?: CommonDaoOptions): Promise<RunQueryResult<TM>>;
99
114
  runQueryCount(opt?: CommonDaoOptions): Promise<number>;
115
+ updateByQuery(patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
100
116
  streamQueryForEach(mapper: AsyncMapper<Saved<BM>, void>, opt?: CommonDaoStreamForEachOptions<Saved<BM>>): Promise<void>;
101
117
  streamQueryAsDBMForEach(mapper: AsyncMapper<DBM, void>, opt?: CommonDaoStreamForEachOptions<DBM>): Promise<void>;
102
118
  streamQuery(opt?: CommonDaoStreamOptions): ReadableTyped<Saved<BM>>;
@@ -85,8 +85,28 @@ class DBQuery {
85
85
  this._endCursor = endCursor;
86
86
  return this;
87
87
  }
88
+ /**
89
+ * Allows to query by ids (one or many).
90
+ * Similar to:
91
+ * SELECT * FROM table where id in (a, b, c)
92
+ * or (if only 1 id is passed)
93
+ * SELECT * FROM table where id = a
94
+ */
95
+ byIds(ids) {
96
+ this._ids = ids;
97
+ return this;
98
+ }
99
+ /**
100
+ * Allows to query by id.
101
+ * Similar to:
102
+ * SELECT * FROM table where id = a
103
+ */
104
+ byId(id) {
105
+ this._ids = [id];
106
+ return this;
107
+ }
88
108
  clone() {
89
- return Object.assign(new DBQuery(this.table), {
109
+ return (0, js_lib_1._objectAssign)(new DBQuery(this.table), {
90
110
  _filters: [...this._filters],
91
111
  _limitValue: this._limitValue,
92
112
  _offsetValue: this._offsetValue,
@@ -96,6 +116,7 @@ class DBQuery {
96
116
  _distinct: this._distinct,
97
117
  _startCursor: this._startCursor,
98
118
  _endCursor: this._endCursor,
119
+ _ids: this._ids,
99
120
  });
100
121
  }
101
122
  pretty() {
@@ -109,6 +130,14 @@ class DBQuery {
109
130
  if (this._selectedFieldNames) {
110
131
  tokens.push(`select${this._distinct ? ' distinct' : ''}(${this._selectedFieldNames.join(',')})`);
111
132
  }
133
+ if (this._ids?.length) {
134
+ if (this._ids.length === 1) {
135
+ tokens.push(`id=${this._ids[0]}`);
136
+ }
137
+ else {
138
+ tokens.push(`ids in (${this._ids.join(',')})`);
139
+ }
140
+ }
112
141
  tokens.push(...this._filters.map(f => `${f.name}${f.op}${f.val}`), ...this._orders.map(o => `order by ${o.name}${o.descending ? ' desc' : ''}`));
113
142
  if (this._groupByFieldNames) {
114
143
  tokens.push(`groupBy(${this._groupByFieldNames.join(',')})`);
@@ -164,6 +193,9 @@ class RunnableDBQuery extends DBQuery {
164
193
  async runQueryCount(opt) {
165
194
  return await this.dao.runQueryCount(this, opt);
166
195
  }
196
+ async updateByQuery(patch, opt) {
197
+ return await this.dao.updateByQuery(this, patch, opt);
198
+ }
167
199
  async streamQueryForEach(mapper, opt) {
168
200
  await this.dao.streamQueryForEach(this, mapper, opt);
169
201
  }
@@ -10,6 +10,8 @@ export interface CommonDBImplementationFeatures {
10
10
  dbQuerySelectFields?: boolean;
11
11
  insert?: boolean;
12
12
  update?: boolean;
13
+ updateByQuery?: boolean;
14
+ dbIncrement?: boolean;
13
15
  createTable?: boolean;
14
16
  tableSchemas?: boolean;
15
17
  /**
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.expectMatch = exports.runCommonDBTest = void 0;
4
4
  const js_lib_1 = require("@naturalcycles/js-lib");
5
5
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
6
+ const db_model_1 = require("../db.model");
6
7
  const dbQuery_1 = require("../query/dbQuery");
7
8
  const dbTransaction_1 = require("../transaction/dbTransaction");
8
9
  const test_model_1 = require("./test.model");
@@ -13,7 +14,7 @@ const test_util_1 = require("./test.util");
13
14
  function runCommonDBTest(db, features = {}, quirks = {}) {
14
15
  const { querying = true, tableSchemas = true, createTable = true, dbQueryFilter = true,
15
16
  // dbQueryFilterIn = true,
16
- dbQueryOrder = true, dbQuerySelectFields = true, insert = true, update = true, streaming = true, strongConsistency = true, bufferSupport = true, nullValues = true, documentDB = true, transactions = true, } = features;
17
+ dbQueryOrder = true, dbQuerySelectFields = true, insert = true, update = true, updateByQuery = true, dbIncrement = true, streaming = true, strongConsistency = true, bufferSupport = true, nullValues = true, documentDB = true, transactions = true, } = features;
17
18
  // const {
18
19
  // allowExtraPropertiesInResponse,
19
20
  // allowBooleansAsUndefined,
@@ -245,6 +246,47 @@ function runCommonDBTest(db, features = {}, quirks = {}) {
245
246
  expectMatch(expected, rows, quirks);
246
247
  });
247
248
  }
249
+ if (updateByQuery) {
250
+ // todo: query by ids (same as getByIds)
251
+ test('updateByQuery simple', async () => {
252
+ // cleanup, reset initial data
253
+ await db.deleteByQuery(queryAll());
254
+ await db.saveBatch(test_model_1.TEST_TABLE, items);
255
+ const patch = {
256
+ k3: 5,
257
+ k2: 'abc',
258
+ };
259
+ await db.updateByQuery(dbQuery_1.DBQuery.create(test_model_1.TEST_TABLE).filterEq('even', true), patch);
260
+ const { rows } = await db.runQuery(queryAll());
261
+ const expected = items.map(r => {
262
+ if (r.even) {
263
+ return { ...r, ...patch };
264
+ }
265
+ return r;
266
+ });
267
+ expectMatch(expected, rows, quirks);
268
+ });
269
+ if (dbIncrement) {
270
+ test('updateByQuery DBIncrement', async () => {
271
+ // cleanup, reset initial data
272
+ await db.deleteByQuery(queryAll());
273
+ await db.saveBatch(test_model_1.TEST_TABLE, items);
274
+ const patch = {
275
+ k3: db_model_1.DBIncrement.of(1),
276
+ k2: 'abcd',
277
+ };
278
+ await db.updateByQuery(dbQuery_1.DBQuery.create(test_model_1.TEST_TABLE).filterEq('even', true), patch);
279
+ const { rows } = await db.runQuery(queryAll());
280
+ const expected = items.map(r => {
281
+ if (r.even) {
282
+ return { ...r, ...patch, k3: (r.k3 || 0) + 1 };
283
+ }
284
+ return r;
285
+ });
286
+ expectMatch(expected, rows, quirks);
287
+ });
288
+ }
289
+ }
248
290
  if (querying) {
249
291
  test('cleanup', async () => {
250
292
  // CLEAN UP
package/package.json CHANGED
@@ -41,7 +41,7 @@
41
41
  "engines": {
42
42
  "node": ">=14.15"
43
43
  },
44
- "version": "8.46.1",
44
+ "version": "8.47.0",
45
45
  "description": "Lowest Common Denominator API to supported Databases",
46
46
  "keywords": [
47
47
  "db",
@@ -7,7 +7,7 @@ import {
7
7
  } from '@naturalcycles/js-lib'
8
8
  import { BaseCommonDB } from '../../base.common.db'
9
9
  import { CommonDB } from '../../common.db'
10
- import { CommonDBOptions, RunQueryResult } from '../../db.model'
10
+ import { CommonDBOptions, DBPatch, RunQueryResult } from '../../db.model'
11
11
  import { DBQuery } from '../../query/dbQuery'
12
12
  import { DBTransaction } from '../../transaction/dbTransaction'
13
13
  import {
@@ -294,6 +294,25 @@ export class CacheDB extends BaseCommonDB implements CommonDB {
294
294
  return deletedIds
295
295
  }
296
296
 
297
+ override async updateByQuery<ROW extends ObjectWithId>(
298
+ q: DBQuery<ROW>,
299
+ patch: DBPatch<ROW>,
300
+ opt: CacheDBOptions = {},
301
+ ): Promise<number> {
302
+ let updated: number | undefined
303
+
304
+ if (!opt.onlyCache && !this.cfg.onlyCache) {
305
+ updated = await this.cfg.downstreamDB.updateByQuery(q, patch, opt)
306
+ }
307
+
308
+ if (!opt.skipCache && !this.cfg.skipCache) {
309
+ const cacheResult = this.cfg.cacheDB.updateByQuery(q, patch, opt)
310
+ if (this.cfg.awaitCache) updated ??= await cacheResult
311
+ }
312
+
313
+ return updated || 0
314
+ }
315
+
297
316
  override async commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void> {
298
317
  await this.cfg.downstreamDB.commitTransaction(tx, opt)
299
318
  await this.cfg.cacheDB.commitTransaction(tx, opt)
@@ -13,6 +13,7 @@ import {
13
13
  _stringMapValues,
14
14
  CommonLogger,
15
15
  _deepCopy,
16
+ _assert,
16
17
  } from '@naturalcycles/js-lib'
17
18
  import {
18
19
  bufferReviver,
@@ -25,7 +26,7 @@ import {
25
26
  } from '@naturalcycles/nodejs-lib'
26
27
  import { dimGrey, yellow } from '@naturalcycles/nodejs-lib/dist/colors'
27
28
  import * as fs from 'fs-extra'
28
- import { CommonDB, DBTransaction, queryInMemory } from '../..'
29
+ import { CommonDB, DBIncrement, DBPatch, DBTransaction, queryInMemory } from '../..'
29
30
  import {
30
31
  CommonDBCreateOptions,
31
32
  CommonDBOptions,
@@ -149,7 +150,7 @@ export class InMemoryDB implements CommonDB {
149
150
  ): Promise<ROW[]> {
150
151
  const table = this.cfg.tablesPrefix + _table
151
152
  this.data[table] ||= {}
152
- return ids.map(id => this.data[table]![id]).filter(Boolean) as ROW[]
153
+ return ids.map(id => this.data[table]![id] as ROW).filter(Boolean)
153
154
  }
154
155
 
155
156
  async saveBatch<ROW extends Partial<ObjectWithId>>(
@@ -190,14 +191,13 @@ export class InMemoryDB implements CommonDB {
190
191
  ): Promise<number> {
191
192
  const table = this.cfg.tablesPrefix + _table
192
193
  this.data[table] ||= {}
193
-
194
- return ids
195
- .map(id => {
196
- const exists = !!this.data[table]![id]
197
- delete this.data[table]![id]
198
- if (exists) return id
199
- })
200
- .filter(Boolean).length
194
+ let count = 0
195
+ ids.forEach(id => {
196
+ if (!this.data[table]![id]) return
197
+ delete this.data[table]![id]
198
+ count++
199
+ })
200
+ return count
201
201
  }
202
202
 
203
203
  async deleteByQuery<ROW extends ObjectWithId>(
@@ -210,6 +210,28 @@ export class InMemoryDB implements CommonDB {
210
210
  return await this.deleteByIds(q.table, ids)
211
211
  }
212
212
 
213
+ async updateByQuery<ROW extends ObjectWithId>(
214
+ q: DBQuery<ROW>,
215
+ patch: DBPatch<ROW>,
216
+ ): Promise<number> {
217
+ const patchEntries = Object.entries(patch)
218
+ if (!patchEntries.length) return 0
219
+
220
+ const table = this.cfg.tablesPrefix + q.table
221
+ const rows = queryInMemory(q, Object.values(this.data[table] || {}) as ROW[])
222
+ rows.forEach((row: any) => {
223
+ patchEntries.forEach(([k, v]) => {
224
+ if (v instanceof DBIncrement) {
225
+ row[k] = (row[k] || 0) + v.amount
226
+ } else {
227
+ row[k] = v
228
+ }
229
+ })
230
+ })
231
+
232
+ return rows.length
233
+ }
234
+
213
235
  async runQuery<ROW extends ObjectWithId>(
214
236
  q: DBQuery<ROW>,
215
237
  _opt?: CommonDBOptions,
@@ -260,9 +282,7 @@ export class InMemoryDB implements CommonDB {
260
282
  * Flushes all tables (all namespaces) at once.
261
283
  */
262
284
  async flushToDisk(): Promise<void> {
263
- if (!this.cfg.persistenceEnabled) {
264
- throw new Error('flushToDisk() called but persistenceEnabled=false')
265
- }
285
+ _assert(this.cfg.persistenceEnabled, 'flushToDisk() called but persistenceEnabled=false')
266
286
  const { persistentStoragePath, persistZip } = this.cfg
267
287
 
268
288
  const started = Date.now()
@@ -297,9 +317,7 @@ export class InMemoryDB implements CommonDB {
297
317
  * Restores all tables (all namespaces) at once.
298
318
  */
299
319
  async restoreFromDisk(): Promise<void> {
300
- if (!this.cfg.persistentStoragePath) {
301
- throw new Error('restoreFromDisk() called but persistenceEnabled=false')
302
- }
320
+ _assert(this.cfg.persistenceEnabled, 'restoreFromDisk() called but persistenceEnabled=false')
303
321
  const { persistentStoragePath } = this.cfg
304
322
 
305
323
  const started = Date.now()
@@ -19,6 +19,11 @@ const FILTER_FNS: Record<DBQueryFilterOperator, FilterFn> = {
19
19
  // Important: q.table is not used in this function, so tablesPrefix is not needed.
20
20
  // But should be careful here..
21
21
  export function queryInMemory<ROW extends ObjectWithId>(q: DBQuery<ROW>, rows: ROW[] = []): ROW[] {
22
+ // .ids
23
+ if (q._ids?.length) {
24
+ rows = rows.filter(r => q._ids!.includes(r.id))
25
+ }
26
+
22
27
  // .filter
23
28
  // eslint-disable-next-line unicorn/no-array-reduce
24
29
  rows = q._filters.reduce((rows, filter) => {
@@ -1,68 +1,76 @@
1
- import { Readable } from 'node:stream'
2
1
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib'
3
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib'
4
3
  import { CommonDB } from './common.db'
5
- import { CommonDBOptions, CommonDBSaveOptions, RunQueryResult } from './db.model'
4
+ import { CommonDBOptions, CommonDBSaveOptions, DBPatch, RunQueryResult } from './db.model'
6
5
  import { DBQuery } from './query/dbQuery'
7
6
  import { DBTransaction } from './transaction/dbTransaction'
8
- import { commitDBTransactionSimple } from './transaction/dbTransaction.util'
7
+
8
+ /* eslint-disable unused-imports/no-unused-vars */
9
9
 
10
10
  /**
11
11
  * No-op implementation of CommonDB interface.
12
12
  * To be extended by actual implementations.
13
13
  */
14
14
  export class BaseCommonDB implements CommonDB {
15
- async ping(): Promise<void> {}
15
+ async ping(): Promise<void> {
16
+ throw new Error('ping is not implemented')
17
+ }
16
18
 
17
19
  async getTables(): Promise<string[]> {
18
- return []
20
+ throw new Error('getTables is not implemented')
19
21
  }
20
22
 
21
23
  async getTableSchema<ROW extends ObjectWithId>(
22
24
  table: string,
23
25
  ): Promise<JsonSchemaRootObject<ROW>> {
24
- return {
25
- $id: `${table}.schema.json`,
26
- type: 'object',
27
- additionalProperties: true,
28
- properties: {} as any,
29
- required: [],
30
- }
26
+ throw new Error('getTableSchema is not implemented')
31
27
  }
32
28
 
33
29
  async createTable<ROW extends ObjectWithId>(
34
- _table: string,
35
- _schema: JsonSchemaObject<ROW>,
36
- ): Promise<void> {}
30
+ table: string,
31
+ schema: JsonSchemaObject<ROW>,
32
+ ): Promise<void> {
33
+ throw new Error('createTable is not implemented')
34
+ }
37
35
 
38
- async deleteByIds<ROW extends ObjectWithId>(_table: string, _ids: ROW['id'][]): Promise<number> {
39
- return 0
36
+ async deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<number> {
37
+ throw new Error('deleteByIds is not implemented')
40
38
  }
41
39
 
42
- async deleteByQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<number> {
43
- return 0
40
+ async deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number> {
41
+ throw new Error('deleteByQuery is not implemented')
44
42
  }
45
43
 
46
- async getByIds<ROW extends ObjectWithId>(_table: string, _ids: ROW['id'][]): Promise<ROW[]> {
47
- return []
44
+ async updateByQuery<ROW extends ObjectWithId>(
45
+ q: DBQuery<ROW>,
46
+ patch: DBPatch<ROW>,
47
+ opt?: CommonDBOptions,
48
+ ): Promise<number> {
49
+ throw new Error('updateByQuery is not implemented')
48
50
  }
49
51
 
50
- async runQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<RunQueryResult<ROW>> {
51
- return { rows: [] }
52
+ async getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<ROW[]> {
53
+ throw new Error('getByIds is not implemented')
52
54
  }
53
55
 
54
- async runQueryCount<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<number> {
55
- return 0
56
+ async runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<RunQueryResult<ROW>> {
57
+ throw new Error('runQuery is not implemented')
58
+ }
59
+
60
+ async runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number> {
61
+ throw new Error('runQueryCount is not implemented')
56
62
  }
57
63
 
58
64
  async saveBatch<ROW extends Partial<ObjectWithId>>(
59
- _table: string,
60
- _rows: ROW[],
61
- _opt?: CommonDBSaveOptions<ROW>,
62
- ): Promise<void> {}
65
+ table: string,
66
+ rows: ROW[],
67
+ opt?: CommonDBSaveOptions<ROW>,
68
+ ): Promise<void> {
69
+ throw new Error('saveBatch is not implemented')
70
+ }
63
71
 
64
- streamQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): ReadableTyped<ROW> {
65
- return Readable.from([])
72
+ streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): ReadableTyped<ROW> {
73
+ throw new Error('streamQuery is not implemented')
66
74
  }
67
75
 
68
76
  /**
@@ -71,6 +79,6 @@ export class BaseCommonDB implements CommonDB {
71
79
  * To be extended.
72
80
  */
73
81
  async commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void> {
74
- await commitDBTransactionSimple(this, tx, opt)
82
+ throw new Error('commitTransaction is not implemented')
75
83
  }
76
84
  }
package/src/common.db.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  CommonDBOptions,
6
6
  CommonDBSaveOptions,
7
7
  CommonDBStreamOptions,
8
+ DBPatch,
8
9
  RunQueryResult,
9
10
  } from './db.model'
10
11
  import { DBQuery } from './query/dbQuery'
@@ -92,6 +93,30 @@ export interface CommonDB {
92
93
 
93
94
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>
94
95
 
96
+ /**
97
+ * Applies patch to the rows returned by the query.
98
+ *
99
+ * Example:
100
+ *
101
+ * UPDATE table SET A = B where $QUERY_CONDITION
102
+ *
103
+ * patch would be { A: 'B' } for that query.
104
+ *
105
+ * Supports "increment query", example:
106
+ *
107
+ * UPDATE table SET A = A + 1
108
+ *
109
+ * In that case patch would look like:
110
+ * { A: DBIncrement(1) }
111
+ *
112
+ * Returns number of rows affected.
113
+ */
114
+ updateByQuery<ROW extends ObjectWithId>(
115
+ q: DBQuery<ROW>,
116
+ patch: DBPatch<ROW>,
117
+ opt?: CommonDBOptions,
118
+ ): Promise<number>
119
+
95
120
  // TRANSACTION
96
121
  /**
97
122
  * Should be implemented as a Transaction (best effort), which means that
@@ -40,6 +40,7 @@ import {
40
40
  DBDeleteByIdsOperation,
41
41
  DBModelType,
42
42
  DBOperation,
43
+ DBPatch,
43
44
  DBSaveBatchOperation,
44
45
  RunQueryResult,
45
46
  } from '../db.model'
@@ -934,6 +935,29 @@ export class CommonDao<
934
935
  return deleted
935
936
  }
936
937
 
938
+ async updateById(id: ID, patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
939
+ return await this.updateByQuery(this.query().byId(id), patch, opt)
940
+ }
941
+
942
+ async updateByIds(ids: ID[], patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
943
+ return await this.updateByQuery(this.query().byIds(ids), patch, opt)
944
+ }
945
+
946
+ async updateByQuery(
947
+ q: DBQuery<DBM>,
948
+ patch: DBPatch<DBM>,
949
+ opt: CommonDaoOptions = {},
950
+ ): Promise<number> {
951
+ this.requireWriteAccess()
952
+ this.requireObjectMutability(opt)
953
+ q.table = opt.table || q.table
954
+ const op = `updateByQuery(${q.pretty()})`
955
+ const started = this.logStarted(op, q.table)
956
+ const updated = await this.cfg.db.updateByQuery(q, patch, opt)
957
+ this.logSaveResult(started, op, q.table)
958
+ return updated
959
+ }
960
+
937
961
  // CONVERSIONS
938
962
 
939
963
  async dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): Promise<undefined>
package/src/db.model.ts CHANGED
@@ -76,3 +76,22 @@ export enum DBModelType {
76
76
  BM = 'BM',
77
77
  TM = 'TM',
78
78
  }
79
+
80
+ /**
81
+ * Allows to construct a query similar to:
82
+ *
83
+ * UPDATE table SET A = A + 1
84
+ *
85
+ * In this case DBIncement.of(1) will be needed.
86
+ */
87
+ export class DBIncrement {
88
+ private constructor(public amount: number) {}
89
+
90
+ static of(amount: number): DBIncrement {
91
+ return new DBIncrement(amount)
92
+ }
93
+ }
94
+
95
+ export type DBPatch<ROW extends Partial<ObjectWithId>> = Partial<
96
+ Record<keyof ROW, ROW[keyof ROW] | DBIncrement>
97
+ >
@@ -5,9 +5,15 @@ import {
5
5
  _truncate,
6
6
  Saved,
7
7
  AnyObject,
8
+ _objectAssign,
8
9
  } from '@naturalcycles/js-lib'
9
10
  import { ReadableTyped } from '@naturalcycles/nodejs-lib'
10
- import { CommonDaoOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions } from '..'
11
+ import {
12
+ CommonDaoOptions,
13
+ CommonDaoStreamForEachOptions,
14
+ CommonDaoStreamOptions,
15
+ DBPatch,
16
+ } from '..'
11
17
  import { CommonDao } from '../commondao/common.dao'
12
18
  import { RunQueryResult } from '../db.model'
13
19
 
@@ -107,6 +113,7 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
107
113
  _selectedFieldNames?: (keyof ROW)[]
108
114
  _groupByFieldNames?: (keyof ROW)[]
109
115
  _distinct = false
116
+ _ids?: ROW['id'][]
110
117
 
111
118
  filter(name: keyof ROW, op: DBQueryFilterOperator, val: any): this {
112
119
  this._filters.push({ name, op, val })
@@ -161,8 +168,30 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
161
168
  return this
162
169
  }
163
170
 
171
+ /**
172
+ * Allows to query by ids (one or many).
173
+ * Similar to:
174
+ * SELECT * FROM table where id in (a, b, c)
175
+ * or (if only 1 id is passed)
176
+ * SELECT * FROM table where id = a
177
+ */
178
+ byIds(ids: ROW['id'][]): this {
179
+ this._ids = ids
180
+ return this
181
+ }
182
+
183
+ /**
184
+ * Allows to query by id.
185
+ * Similar to:
186
+ * SELECT * FROM table where id = a
187
+ */
188
+ byId(id: ROW['id']): this {
189
+ this._ids = [id]
190
+ return this
191
+ }
192
+
164
193
  clone(): DBQuery<ROW> {
165
- return Object.assign(new DBQuery<ROW>(this.table), {
194
+ return _objectAssign(new DBQuery<ROW>(this.table), {
166
195
  _filters: [...this._filters],
167
196
  _limitValue: this._limitValue,
168
197
  _offsetValue: this._offsetValue,
@@ -172,6 +201,7 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
172
201
  _distinct: this._distinct,
173
202
  _startCursor: this._startCursor,
174
203
  _endCursor: this._endCursor,
204
+ _ids: this._ids,
175
205
  })
176
206
  }
177
207
 
@@ -192,6 +222,14 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
192
222
  )
193
223
  }
194
224
 
225
+ if (this._ids?.length) {
226
+ if (this._ids.length === 1) {
227
+ tokens.push(`id=${this._ids[0]}`)
228
+ } else {
229
+ tokens.push(`ids in (${this._ids.join(',')})`)
230
+ }
231
+ }
232
+
195
233
  tokens.push(
196
234
  ...this._filters.map(f => `${f.name as string}${f.op}${f.val}`),
197
235
  ...this._orders.map(o => `order by ${o.name as string}${o.descending ? ' desc' : ''}`),
@@ -269,6 +307,10 @@ export class RunnableDBQuery<
269
307
  return await this.dao.runQueryCount(this, opt)
270
308
  }
271
309
 
310
+ async updateByQuery(patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number> {
311
+ return await this.dao.updateByQuery(this, patch, opt)
312
+ }
313
+
272
314
  async streamQueryForEach(
273
315
  mapper: AsyncMapper<Saved<BM>, void>,
274
316
  opt?: CommonDaoStreamForEachOptions<Saved<BM>>,
@@ -1,6 +1,7 @@
1
1
  import { pDelay, pMap, _filterObject, _pick, _sortBy } from '@naturalcycles/js-lib'
2
2
  import { readableToArray } from '@naturalcycles/nodejs-lib'
3
3
  import { CommonDB } from '../common.db'
4
+ import { DBIncrement, DBPatch } from '../db.model'
4
5
  import { DBQuery } from '../query/dbQuery'
5
6
  import { DBTransaction } from '../transaction/dbTransaction'
6
7
  import {
@@ -25,6 +26,10 @@ export interface CommonDBImplementationFeatures {
25
26
  insert?: boolean
26
27
  update?: boolean
27
28
 
29
+ updateByQuery?: boolean
30
+
31
+ dbIncrement?: boolean
32
+
28
33
  createTable?: boolean
29
34
  tableSchemas?: boolean
30
35
 
@@ -87,6 +92,8 @@ export function runCommonDBTest(
87
92
  dbQuerySelectFields = true,
88
93
  insert = true,
89
94
  update = true,
95
+ updateByQuery = true,
96
+ dbIncrement = true,
90
97
  streaming = true,
91
98
  strongConsistency = true,
92
99
  bufferSupport = true,
@@ -383,6 +390,58 @@ export function runCommonDBTest(
383
390
  })
384
391
  }
385
392
 
393
+ if (updateByQuery) {
394
+ // todo: query by ids (same as getByIds)
395
+ test('updateByQuery simple', async () => {
396
+ // cleanup, reset initial data
397
+ await db.deleteByQuery(queryAll())
398
+ await db.saveBatch(TEST_TABLE, items)
399
+
400
+ const patch: DBPatch<TestItemDBM> = {
401
+ k3: 5,
402
+ k2: 'abc',
403
+ }
404
+
405
+ await db.updateByQuery(DBQuery.create<TestItemDBM>(TEST_TABLE).filterEq('even', true), patch)
406
+
407
+ const { rows } = await db.runQuery(queryAll())
408
+ const expected = items.map(r => {
409
+ if (r.even) {
410
+ return { ...r, ...patch }
411
+ }
412
+ return r
413
+ })
414
+ expectMatch(expected, rows, quirks)
415
+ })
416
+
417
+ if (dbIncrement) {
418
+ test('updateByQuery DBIncrement', async () => {
419
+ // cleanup, reset initial data
420
+ await db.deleteByQuery(queryAll())
421
+ await db.saveBatch(TEST_TABLE, items)
422
+
423
+ const patch: DBPatch<TestItemDBM> = {
424
+ k3: DBIncrement.of(1),
425
+ k2: 'abcd',
426
+ }
427
+
428
+ await db.updateByQuery(
429
+ DBQuery.create<TestItemDBM>(TEST_TABLE).filterEq('even', true),
430
+ patch,
431
+ )
432
+
433
+ const { rows } = await db.runQuery(queryAll())
434
+ const expected = items.map(r => {
435
+ if (r.even) {
436
+ return { ...r, ...patch, k3: (r.k3 || 0) + 1 }
437
+ }
438
+ return r
439
+ })
440
+ expectMatch(expected, rows, quirks)
441
+ })
442
+ }
443
+ }
444
+
386
445
  if (querying) {
387
446
  test('cleanup', async () => {
388
447
  // CLEAN UP