-
Notifications
You must be signed in to change notification settings - Fork 208
/
SqliteChangesetReader.ts
386 lines (358 loc) · 12.7 KB
/
SqliteChangesetReader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module SQLiteDb
*/
import { IModelJsNative } from "@bentley/imodeljs-native";
import { DbChangeStage, DbOpcode, DbResult, DbValueType, Id64String, IDisposable } from "@itwin/core-bentley";
import { ECDb } from "./ECDb";
import { IModelDb } from "./IModelDb";
import { IModelHost } from "./IModelHost";
/** Changed value type
* @beta
*/
type SqliteValue = Uint8Array | number | string | null | undefined;
/** Array of changed values
* @beta
*/
type SqliteValueArray = SqliteValue[];
/**
* Format option when converting change from array to column/value object.
* @beta
*/
export interface ChangeFormatArgs {
/** include table name */
includeTableName?: true;
/** include op code */
includeOpCode?: true;
/** include null columns */
includeNullColumns?: true;
/** include value version */
includeStage?: true;
/** include primary key in update change */
includePrimaryKeyInUpdateNew?: true;
}
/** Operation that cause the change
* @beta
*/
export type SqliteChangeOp = "Inserted" | "Updated" | "Deleted";
/** Stage is version of value that needed to be read
* @beta
*/
export type SqliteValueStage = "Old" | "New";
/** Db from which schema will be read. It should be from timeline to which changeset belong.
* @beta
*/
export type AnyDb = IModelDb | ECDb;
/** Arg to open a changeset file from disk
* @beta
*/
export interface SqliteChangesetReaderArgs {
/** db from which schema will be read. It should be close to changeset.*/
readonly db?: AnyDb;
/** invert the changeset operations */
readonly invert?: true;
/** do not check if column of change match db schema instead ignore addition columns */
readonly disableSchemaCheck?: true;
}
/**
* Represent sqlite change.
* @beta
*/
export interface SqliteChange {
/** name of table */
$table?: string;
/** SQLite operation that created this change */
$op?: SqliteChangeOp;
/** version of data in change. */
$stage?: SqliteValueStage;
/** columns in change */
[key: string]: any;
}
/**
* Read raw sqlite changeset from disk and enumerate changes.
* It also optionally let you format change with schema from
* a db provided.
* @beta
*/
export class SqliteChangesetReader implements IDisposable {
private readonly _nativeReader = new IModelHost.platform.ChangesetReader();
private _schemaCache = new Map<string, string[]>();
private _disableSchemaCheck = false;
private _changeIndex = 0;
protected constructor(
/** db from where sql schema will be read */
public readonly db?: AnyDb,
) { }
/**
* Open changeset file from disk
* @param args fileName of changeset reader and other options.
* @returns SqliteChangesetReader instance
*/
public static openFile(args: { readonly fileName: string } & SqliteChangesetReaderArgs): SqliteChangesetReader {
const reader = new SqliteChangesetReader(args.db);
reader._disableSchemaCheck = args.disableSchemaCheck ?? false;
reader._nativeReader.openFile(args.fileName, args.invert ?? false);
return reader;
}
/**
* Open local changes in iModel.
* @param args iModel and other options.
* @returns SqliteChangesetReader instance
*/
public static openLocalChanges(args: { iModel: IModelJsNative.DgnDb, includeInMemoryChanges?: true } & SqliteChangesetReaderArgs): SqliteChangesetReader {
const reader = new SqliteChangesetReader(args.db);
reader._disableSchemaCheck = args.disableSchemaCheck ?? false;
reader._nativeReader.openLocalChanges(args.iModel, args.includeInMemoryChanges ?? false, args.invert ?? false);
return reader;
}
/** check if schema check is disabled or not */
public get disableSchemaCheck(): boolean { return this._disableSchemaCheck; }
/** Move to next change in changeset
* @returns true if there is current change false if reader is end of changeset.
* @beta
*/
public step(): boolean {
if (this._nativeReader.step()) {
this._changeIndex++;
return true;
}
return false;
}
/** Check if reader current on a row
* @beta
*/
public get hasRow(): boolean {
return this._nativeReader.hasRow();
}
/** Check if its current change is indirect
* @beta
*/
public get isIndirect(): boolean {
return this._nativeReader.isIndirectChange();
}
/** Get count of columns in current change
* @beta
*/
public get columnCount(): number {
return this._nativeReader.getColumnCount();
}
/** Get operation that caused the change
* @beta
*/
public get op(): SqliteChangeOp {
if (this._nativeReader.getOpCode() === DbOpcode.Insert)
return "Inserted";
if (this._nativeReader.getOpCode() === DbOpcode.Delete)
return "Deleted";
return "Updated";
}
/** Get primary key value array
* @beta
*/
public get primaryKeyValues(): SqliteValueArray {
return this._nativeReader.getPrimaryKeys();
}
/** Get primary key columns.
* @note To this to work db arg must be set when opening changeset file.
* @beta
*/
public getPrimaryKeyColumnNames(): string[] {
const cols = this.getColumnNames(this.tableName);
if (!this._disableSchemaCheck && cols.length !== this.columnCount)
throw new Error(`changeset table ${this.tableName} columns count does not match db declared table. ${this.columnCount} <> ${cols.length}`);
return this._nativeReader.getPrimaryKeyColumnIndexes().map((i) => cols[i]);
}
/** Get current change table.
* @beta
*/
public get tableName(): string {
return this._nativeReader.getTableName();
}
/**
* Get changed binary value for a column
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns value for changed column
* @beta
*/
public getChangeValueType(columnIndex: number, stage: SqliteValueStage): DbValueType | undefined {
return this._nativeReader.getColumnValueType(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old) as DbValueType;
}
/**
* Get changed binary value for a column
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns value for changed column
* @beta
*/
public getChangeValueBinary(columnIndex: number, stage: SqliteValueStage): Uint8Array | null | undefined {
return this._nativeReader.getColumnValueBinary(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Get changed double value for a column
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns value for changed column
* @beta
*/
public getChangeValueDouble(columnIndex: number, stage: SqliteValueStage): number | null | undefined {
return this._nativeReader.getColumnValueDouble(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Get changed Id value for a column
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns value for changed column
* @beta
*/
public getChangeValueId(columnIndex: number, stage: SqliteValueStage): Id64String | null | undefined {
return this._nativeReader.getColumnValueId(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Get changed integer value for a column
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns value for changed column
* @beta
*/
public getChangeValueInteger(columnIndex: number, stage: SqliteValueStage): number | null | undefined {
return this._nativeReader.getColumnValueInteger(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Get changed text value for a column
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns value for changed column
* @beta
*/
public getChangeValueText(columnIndex: number, stage: SqliteValueStage): string | null | undefined {
return this._nativeReader.getColumnValueText(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Check if change value is null
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns true if value is null
* @beta
*/
public isColumnValueNull(columnIndex: number, stage: SqliteValueStage): boolean | undefined {
return this._nativeReader.isColumnValueNull(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Get change value type
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns change value type
* @beta
*/
public getColumnValueType(columnIndex: number, stage: SqliteValueStage): DbValueType | undefined {
return this._nativeReader.getColumnValueType(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old) as DbValueType | undefined;
}
/**
* Get changed value for a column
* @param columnIndex index of column in current change
* @param stage old or new value for change.
* @returns value for changed column
* @beta
*/
public getChangeValue(columnIndex: number, stage: SqliteValueStage): SqliteValue {
return this._nativeReader.getColumnValue(columnIndex, stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Get all changed value in current change as array
* @param stage old or new values for current change.
* @returns array of values.
* @beta
*/
public getChangeValuesArray(stage: SqliteValueStage): SqliteValueArray | undefined {
return this._nativeReader.getRow(stage === "New" ? DbChangeStage.New : DbChangeStage.Old);
}
/**
* Get change as object and format its content.
* @param stage old or new value for current change.
* @param args change format options
* @returns return object or undefined
* @beta
*/
public getChangeValuesObject(stage: SqliteValueStage, args: ChangeFormatArgs = {}): SqliteChange | undefined {
const cols = this.getColumnNames(this.tableName);
const row = this.getChangeValuesArray(stage);
if (!row)
return undefined;
process.env;
const minLen = Math.min(cols.length, row.length);
if (!this._disableSchemaCheck && cols.length !== this.columnCount)
throw new Error(`changeset table ${this.tableName} columns count does not match db declared table. ${this.columnCount} <> ${cols.length}`);
const out: SqliteChange = {};
if (args.includeTableName) {
out.$table = this.tableName;
}
if (args.includeOpCode) {
out.$op = this.op;
}
if (args.includeStage) {
out.$stage = stage;
}
if (args.includePrimaryKeyInUpdateNew && this.op === "Updated" && stage === "New") {
const pkNames = this.getPrimaryKeyColumnNames();
const pkValues = this.primaryKeyValues;
pkNames.forEach((v, i) => {
out[v] = pkValues[i];
});
}
const isNullOrUndefined = (val: SqliteValue) => typeof val === "undefined";
for (let i = 0; i < minLen; ++i) {
const columnValue = row[i];
const columnName = cols[i];
if (!args.includeNullColumns && isNullOrUndefined(columnValue))
continue;
out[columnName] = columnValue;
}
return out;
}
/**
* Get list of column for a table. This function also caches the result.
* @note To this to work db arg must be set when opening changeset file.
* @param tableName name of the table for which columns are requested.
* @returns columns of table.
* @beta
*/
public getColumnNames(tableName: string): string[] {
const columns = this._schemaCache.get(tableName);
if (columns)
return columns;
if (!this.db)
throw new Error("getColumns() require db context to be provided.");
return this.db.withPreparedSqliteStatement("SELECT [name] FROM PRAGMA_TABLE_INFO(?) ORDER BY [cid]", (stmt) => {
stmt.bindString(1, tableName);
const tblCols: string[] = [];
while (stmt.step() === DbResult.BE_SQLITE_ROW) {
tblCols.push(stmt.getValueString(0));
}
this._schemaCache.set(tableName, tblCols);
return tblCols;
});
}
/** index of current change
* @beta
*/
public get changeIndex() { return this._changeIndex; }
/**
* Close changeset
* @beta
*/
public close() {
this._changeIndex = 0;
this._nativeReader.close();
}
/**
* Dispose this object
* @beta
*/
public dispose(): void {
this.close();
}
}