@abaplint/runtime 2.13.95 → 2.13.96
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/build/src/context.d.ts +5 -0
- package/build/src/context.js +4 -0
- package/build/src/dataset/dataset.d.ts +51 -0
- package/build/src/dataset/dataset.js +3 -0
- package/build/src/dataset/memory_dataset.d.ts +17 -0
- package/build/src/dataset/memory_dataset.js +46 -0
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +4 -1
- package/build/src/primary_key.d.ts +2 -2
- package/build/src/statements/collect.d.ts +2 -2
- package/build/src/statements/collect.js +51 -12
- package/build/src/statements/dataset.d.ts +23 -0
- package/build/src/statements/dataset.js +343 -0
- package/build/src/statements/index.d.ts +11 -0
- package/build/src/statements/index.js +22 -0
- package/build/src/statements/loop.js +71 -0
- package/package.json +1 -1
package/build/src/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Console } from "./console/console";
|
|
2
2
|
import { DatabaseClient, DatabaseCursorCallbacks } from "./db/db";
|
|
3
3
|
import * as RFC from "./rfc";
|
|
4
|
+
import { DatasetHost, OpenDataset } from "./dataset/dataset";
|
|
4
5
|
export declare class Context {
|
|
5
6
|
console: Console;
|
|
6
7
|
cursorCounter: number;
|
|
@@ -13,5 +14,9 @@ export declare class Context {
|
|
|
13
14
|
RFCDestinations: {
|
|
14
15
|
[name: string]: RFC.RFCClient;
|
|
15
16
|
};
|
|
17
|
+
dataset: DatasetHost | undefined;
|
|
18
|
+
datasets: {
|
|
19
|
+
[name: string]: OpenDataset;
|
|
20
|
+
};
|
|
16
21
|
defaultDB(): DatabaseClient;
|
|
17
22
|
}
|
package/build/src/context.js
CHANGED
|
@@ -8,6 +8,10 @@ class Context {
|
|
|
8
8
|
// DEFAULT and secondary database connections
|
|
9
9
|
databaseConnections = {};
|
|
10
10
|
RFCDestinations = {};
|
|
11
|
+
// the file system OPEN DATASET and friends read and write; none by default,
|
|
12
|
+
// and then every DATASET statement throws
|
|
13
|
+
dataset = undefined;
|
|
14
|
+
datasets = {};
|
|
11
15
|
defaultDB() {
|
|
12
16
|
if (this.databaseConnections["DEFAULT"] === undefined) {
|
|
13
17
|
throw new Error("Runtime, database not initialized");
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { FieldSymbol } from "../types";
|
|
2
|
+
import { ICharacter } from "../types/_character";
|
|
3
|
+
import { INumeric } from "../types/_numeric";
|
|
4
|
+
export type DatasetMode = "INPUT" | "OUTPUT" | "APPENDING" | "UPDATE";
|
|
5
|
+
/** an open file: positional reads and writes, the runtime keeps the position */
|
|
6
|
+
export interface DatasetHandle {
|
|
7
|
+
/** up to length bytes from position; fewer at the end of the file */
|
|
8
|
+
read(position: number, length: number): Promise<Uint8Array>;
|
|
9
|
+
write(position: number, bytes: Uint8Array): Promise<void>;
|
|
10
|
+
size(): Promise<number>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface DatasetHost {
|
|
14
|
+
/** OUTPUT truncates or creates, APPENDING and UPDATE keep the content;
|
|
15
|
+
* a failure is returned as the text OPEN DATASET ... MESSAGE receives
|
|
16
|
+
* and becomes sy-subrc 8 */
|
|
17
|
+
open(name: string, mode: DatasetMode): Promise<DatasetHandle | {
|
|
18
|
+
message: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** false when there was nothing to delete (sy-subrc 4) */
|
|
21
|
+
delete(name: string): Promise<boolean>;
|
|
22
|
+
}
|
|
23
|
+
export interface IOpenDatasetOptions {
|
|
24
|
+
mode: DatasetMode;
|
|
25
|
+
binary: boolean;
|
|
26
|
+
encoding?: "DEFAULT" | "UTF-8" | "NON-UNICODE";
|
|
27
|
+
legacy?: boolean;
|
|
28
|
+
message?: ICharacter | FieldSymbol;
|
|
29
|
+
position?: INumeric | FieldSymbol;
|
|
30
|
+
/** additions this runtime does not implement, by name */
|
|
31
|
+
unsupported?: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface ITransferOptions {
|
|
34
|
+
length?: INumeric | FieldSymbol;
|
|
35
|
+
noEndOfLine?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface IReadDatasetOptions {
|
|
38
|
+
maximumLength?: INumeric | FieldSymbol;
|
|
39
|
+
actualLength?: INumeric | FieldSymbol;
|
|
40
|
+
}
|
|
41
|
+
export interface IGetDatasetOptions {
|
|
42
|
+
position?: INumeric | FieldSymbol;
|
|
43
|
+
attributes?: any;
|
|
44
|
+
}
|
|
45
|
+
/** a file OPEN DATASET opened, as the runtime keeps it by name */
|
|
46
|
+
export interface OpenDataset {
|
|
47
|
+
handle: DatasetHandle;
|
|
48
|
+
mode: DatasetMode;
|
|
49
|
+
binary: boolean;
|
|
50
|
+
position: number;
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DatasetHandle, DatasetHost, DatasetMode } from "./dataset";
|
|
2
|
+
/** A DatasetHost over a map of names to bytes: for tests, and for a runtime
|
|
3
|
+
* in a browser, where there is no file system to hand to OPEN DATASET.
|
|
4
|
+
* OUTPUT empties or creates a file, APPENDING and UPDATE create it when
|
|
5
|
+
* missing, INPUT of a missing file fails with the message a system gives. */
|
|
6
|
+
export declare class MemoryDataset implements DatasetHost {
|
|
7
|
+
readonly files: {
|
|
8
|
+
[name: string]: Uint8Array;
|
|
9
|
+
};
|
|
10
|
+
constructor(files?: {
|
|
11
|
+
[name: string]: Uint8Array;
|
|
12
|
+
});
|
|
13
|
+
open(name: string, mode: DatasetMode): Promise<DatasetHandle | {
|
|
14
|
+
message: string;
|
|
15
|
+
}>;
|
|
16
|
+
delete(name: string): Promise<boolean>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryDataset = void 0;
|
|
4
|
+
/** A DatasetHost over a map of names to bytes: for tests, and for a runtime
|
|
5
|
+
* in a browser, where there is no file system to hand to OPEN DATASET.
|
|
6
|
+
* OUTPUT empties or creates a file, APPENDING and UPDATE create it when
|
|
7
|
+
* missing, INPUT of a missing file fails with the message a system gives. */
|
|
8
|
+
class MemoryDataset {
|
|
9
|
+
files;
|
|
10
|
+
constructor(files = {}) {
|
|
11
|
+
this.files = files;
|
|
12
|
+
}
|
|
13
|
+
async open(name, mode) {
|
|
14
|
+
const files = this.files;
|
|
15
|
+
if (files[name] === undefined) {
|
|
16
|
+
if (mode === "INPUT") {
|
|
17
|
+
return { message: "No such file or directory" };
|
|
18
|
+
}
|
|
19
|
+
files[name] = new Uint8Array(0);
|
|
20
|
+
}
|
|
21
|
+
if (mode === "OUTPUT") {
|
|
22
|
+
files[name] = new Uint8Array(0);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
read: async (position, length) => files[name].slice(position, position + length),
|
|
26
|
+
write: async (position, bytes) => {
|
|
27
|
+
const end = Math.max(files[name].length, position + bytes.length);
|
|
28
|
+
const out = new Uint8Array(end);
|
|
29
|
+
out.set(files[name], 0);
|
|
30
|
+
out.set(bytes, position);
|
|
31
|
+
files[name] = out;
|
|
32
|
+
},
|
|
33
|
+
size: async () => files[name].length,
|
|
34
|
+
close: async () => undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async delete(name) {
|
|
38
|
+
if (this.files[name] === undefined) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
delete this.files[name];
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.MemoryDataset = MemoryDataset;
|
|
46
|
+
//# sourceMappingURL=memory_dataset.js.map
|
package/build/src/index.d.ts
CHANGED
|
@@ -21,7 +21,10 @@ import { ABAPEventing } from "./abap_eventing";
|
|
|
21
21
|
import { isLineNotFound } from "./is_line_not_found";
|
|
22
22
|
import { parametersCall } from "./parameters_call";
|
|
23
23
|
import { alphaIn, alphaOut } from "./alpha";
|
|
24
|
+
import { DatasetHost } from "./dataset/dataset";
|
|
24
25
|
export { RFC, types, DB, MemoryConsole };
|
|
26
|
+
export type { DatasetHost, DatasetHandle, DatasetMode } from "./dataset/dataset";
|
|
27
|
+
export { MemoryDataset } from "./dataset/memory_dataset";
|
|
25
28
|
export type RuntimeDatabaseOptions = {
|
|
26
29
|
schemaPrefix?: string;
|
|
27
30
|
tablePrefix?: string;
|
|
@@ -29,6 +32,7 @@ export type RuntimeDatabaseOptions = {
|
|
|
29
32
|
export type RuntimeOptions = {
|
|
30
33
|
console?: Console;
|
|
31
34
|
database?: RuntimeDatabaseOptions;
|
|
35
|
+
dataset?: DatasetHost;
|
|
32
36
|
};
|
|
33
37
|
export declare class ABAP {
|
|
34
38
|
Classes: {
|
package/build/src/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ABAP = exports.MemoryConsole = exports.DB = exports.types = exports.RFC = void 0;
|
|
36
|
+
exports.ABAP = exports.MemoryDataset = exports.MemoryConsole = exports.DB = exports.types = exports.RFC = void 0;
|
|
37
37
|
const context_1 = require("./context");
|
|
38
38
|
const offset_length_1 = require("./offset_length");
|
|
39
39
|
const statements_1 = require("./statements");
|
|
@@ -61,6 +61,8 @@ const abap_eventing_1 = require("./abap_eventing");
|
|
|
61
61
|
const is_line_not_found_1 = require("./is_line_not_found");
|
|
62
62
|
const parameters_call_1 = require("./parameters_call");
|
|
63
63
|
const alpha_1 = require("./alpha");
|
|
64
|
+
var memory_dataset_1 = require("./dataset/memory_dataset");
|
|
65
|
+
Object.defineProperty(exports, "MemoryDataset", { enumerable: true, get: function () { return memory_dataset_1.MemoryDataset; } });
|
|
64
66
|
class ABAP {
|
|
65
67
|
// global objects
|
|
66
68
|
Classes = {};
|
|
@@ -101,6 +103,7 @@ class ABAP {
|
|
|
101
103
|
this.context = new context_1.Context();
|
|
102
104
|
this.console = input?.console ? input?.console : new standard_out_console_1.StandardOutConsole();
|
|
103
105
|
this.context.console = this.console;
|
|
106
|
+
this.context.dataset = input?.dataset;
|
|
104
107
|
this.dbo = input?.database || { schemaPrefix: "", tablePrefix: "" };
|
|
105
108
|
if (this.dbo.schemaPrefix === undefined) {
|
|
106
109
|
this.dbo.schemaPrefix = "";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Table } from "./types";
|
|
2
|
-
export declare function primaryKeyValues(target: Table, row: any): any[];
|
|
1
|
+
import { HashedTable, Table } from "./types";
|
|
2
|
+
export declare function primaryKeyValues(target: Table | HashedTable, row: any): any[];
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Table } from "../types";
|
|
1
|
+
import { HashedTable, Structure, Table } from "../types";
|
|
2
2
|
import { ICharacter } from "../types/_character";
|
|
3
|
-
export declare function collect(source: ICharacter | Table, target?: Table): void;
|
|
3
|
+
export declare function collect(source: ICharacter | Structure | Table, target?: Table | HashedTable): void;
|
|
@@ -2,25 +2,64 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.collect = collect;
|
|
4
4
|
const compare_1 = require("../compare");
|
|
5
|
+
const primary_key_1 = require("../primary_key");
|
|
5
6
|
const types_1 = require("../types");
|
|
6
7
|
const insert_internal_1 = require("./insert_internal");
|
|
7
|
-
|
|
8
|
+
function sumNumeric(found, source, keyValues) {
|
|
9
|
+
if (found instanceof types_1.Structure && source instanceof types_1.Structure) {
|
|
10
|
+
for (const name of Object.keys(source.get())) {
|
|
11
|
+
sumNumeric(found.get()[name], source.get()[name], keyValues);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else if (keyValues.has(source)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
else if (found instanceof types_1.Integer && source instanceof types_1.Integer) {
|
|
18
|
+
found.set(found.get() + source.get());
|
|
19
|
+
}
|
|
20
|
+
else if (found instanceof types_1.Integer8 && source instanceof types_1.Integer8) {
|
|
21
|
+
found.set(found.get() + source.get());
|
|
22
|
+
}
|
|
23
|
+
else if (found instanceof types_1.Packed && source instanceof types_1.Packed) {
|
|
24
|
+
const decimals = found.getDecimals();
|
|
25
|
+
const factor = 10n ** BigInt(decimals);
|
|
26
|
+
const left = BigInt(found.toFixed(decimals).replace(".", ""));
|
|
27
|
+
const right = BigInt(source.toFixed(decimals).replace(".", ""));
|
|
28
|
+
const total = left + right;
|
|
29
|
+
const magnitude = total < 0n ? -total : total;
|
|
30
|
+
const fraction = decimals === 0 ? "" : "." + (magnitude % factor).toString().padStart(decimals, "0");
|
|
31
|
+
found.set((total < 0n ? "-" : "") + (magnitude / factor).toString() + fraction);
|
|
32
|
+
}
|
|
33
|
+
else if (found instanceof types_1.Float && source instanceof types_1.Float) {
|
|
34
|
+
found.set(found.getRaw() + source.getRaw());
|
|
35
|
+
}
|
|
36
|
+
else if (found instanceof types_1.DecFloat34 && source instanceof types_1.DecFloat34) {
|
|
37
|
+
found.set(found.getRaw() + source.getRaw());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
8
40
|
function collect(source, target) {
|
|
9
41
|
if (target === undefined && source instanceof types_1.Table) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (read.subrc === 4) {
|
|
13
|
-
(0, insert_internal_1.insertInternal)({ table: source, data: source.getHeader() });
|
|
14
|
-
}
|
|
42
|
+
target = source;
|
|
43
|
+
source = source.getHeader();
|
|
15
44
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
if (target === undefined) {
|
|
46
|
+
throw new Error("COLLECT, no target specified");
|
|
47
|
+
}
|
|
48
|
+
const sourceKeys = (0, primary_key_1.primaryKeyValues)(target, source);
|
|
49
|
+
const matches = (row) => {
|
|
50
|
+
const rowKeys = (0, primary_key_1.primaryKeyValues)(target, row);
|
|
51
|
+
return rowKeys.length === sourceKeys.length && rowKeys.every((key, index) => (0, compare_1.eq)(key, sourceKeys[index]));
|
|
52
|
+
};
|
|
53
|
+
const rows = target.array();
|
|
54
|
+
let index = rows.findIndex(matches);
|
|
55
|
+
if (index >= 0) {
|
|
56
|
+
sumNumeric(rows[index], source, new Set(sourceKeys));
|
|
57
|
+
abap.builtin.sy.get().subrc.set(0);
|
|
21
58
|
}
|
|
22
59
|
else {
|
|
23
|
-
|
|
60
|
+
(0, insert_internal_1.insertInternal)({ table: target, data: source });
|
|
61
|
+
index = target.array().findIndex(matches);
|
|
24
62
|
}
|
|
63
|
+
abap.builtin.sy.get().tabix.set(target instanceof types_1.HashedTable ? 0 : index + 1);
|
|
25
64
|
}
|
|
26
65
|
//# sourceMappingURL=collect.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Context } from "../context";
|
|
2
|
+
import { FieldSymbol } from "../types";
|
|
3
|
+
import { ICharacter } from "../types/_character";
|
|
4
|
+
import { INumeric } from "../types/_numeric";
|
|
5
|
+
import { IGetDatasetOptions, IOpenDatasetOptions, IReadDatasetOptions, ITransferOptions } from "../dataset/dataset";
|
|
6
|
+
export declare class DatasetStatements {
|
|
7
|
+
private readonly context;
|
|
8
|
+
constructor(context: Context);
|
|
9
|
+
private host;
|
|
10
|
+
private opened;
|
|
11
|
+
openDataset(nameIn: ICharacter | FieldSymbol | string, options: IOpenDatasetOptions): Promise<void>;
|
|
12
|
+
closeDataset(nameIn: ICharacter | FieldSymbol | string): Promise<void>;
|
|
13
|
+
deleteDataset(nameIn: ICharacter | FieldSymbol | string): Promise<void>;
|
|
14
|
+
transfer(sourceIn: any, nameIn: ICharacter | FieldSymbol | string, options?: ITransferOptions): Promise<void>;
|
|
15
|
+
readDataset(nameIn: ICharacter | FieldSymbol | string, targetIn: any, options?: IReadDatasetOptions): Promise<void>;
|
|
16
|
+
private readLine;
|
|
17
|
+
private readBytes;
|
|
18
|
+
getDataset(nameIn: ICharacter | FieldSymbol | string, options: IGetDatasetOptions): Promise<void>;
|
|
19
|
+
setDataset(nameIn: ICharacter | FieldSymbol | string, options: {
|
|
20
|
+
position?: INumeric | FieldSymbol;
|
|
21
|
+
endOfFile?: boolean;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatasetStatements = void 0;
|
|
4
|
+
const throw_error_1 = require("../throw_error");
|
|
5
|
+
const types_1 = require("../types");
|
|
6
|
+
// OPEN / READ / TRANSFER / CLOSE / DELETE / GET / SET DATASET.
|
|
7
|
+
//
|
|
8
|
+
// The ABAP semantics live here, once: text lines, the byte layout of each
|
|
9
|
+
// mode, ACTUAL LENGTH, sy-subrc and the exceptions. A host supplies bytes
|
|
10
|
+
// only (DatasetHost), the way a DatabaseClient supplies rows, so that
|
|
11
|
+
// a sandboxed file system, an in-memory one in a browser and a test double
|
|
12
|
+
// all behave the same. Without a host every statement throws, as before.
|
|
13
|
+
// The host and the options are defined in ../dataset/dataset.ts, and an
|
|
14
|
+
// in-memory host is ../dataset/memory_dataset.ts.
|
|
15
|
+
// what the text mode reads per host call while it looks for the end of a line
|
|
16
|
+
const CHUNK = 64 * 1024;
|
|
17
|
+
function deref(value) {
|
|
18
|
+
if (value instanceof types_1.FieldSymbol) {
|
|
19
|
+
const pointer = value.getPointer();
|
|
20
|
+
if (pointer === undefined) {
|
|
21
|
+
throw new Error("GETWA_NOT_ASSIGNED");
|
|
22
|
+
}
|
|
23
|
+
return pointer;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function nameOf(name) {
|
|
28
|
+
const value = typeof name === "string" ? name : deref(name).get();
|
|
29
|
+
// a C field holding the name is padded; the name is not
|
|
30
|
+
return (value + "").replace(/ +$/, "");
|
|
31
|
+
}
|
|
32
|
+
function numberOf(value) {
|
|
33
|
+
return value === undefined ? undefined : Number(deref(value).get());
|
|
34
|
+
}
|
|
35
|
+
function setSubrc(value) {
|
|
36
|
+
abap.builtin.sy.get().subrc.set(value);
|
|
37
|
+
}
|
|
38
|
+
function isHex(target) {
|
|
39
|
+
return target instanceof types_1.Hex || target instanceof types_1.HexUInt8;
|
|
40
|
+
}
|
|
41
|
+
function isCharLike(target) {
|
|
42
|
+
return typeof target?.get === "function" && typeof target.get() === "string" && !isHex(target) && !(target instanceof types_1.XString);
|
|
43
|
+
}
|
|
44
|
+
function hexToBytes(hex) {
|
|
45
|
+
const out = new Uint8Array(Math.floor(hex.length / 2));
|
|
46
|
+
for (let i = 0; i < out.length; i++) {
|
|
47
|
+
out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function bytesToHex(bytes) {
|
|
52
|
+
let out = "";
|
|
53
|
+
for (const b of bytes) {
|
|
54
|
+
out += b.toString(16).toUpperCase().padStart(2, "0");
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
// a character-like field in BINARY MODE is its code units in the system code
|
|
59
|
+
// page, UTF-16LE on a Unicode system (measured: c(4) 'ab' is 6100 6200
|
|
60
|
+
// 2000 2000)
|
|
61
|
+
function utf16le(text) {
|
|
62
|
+
const out = new Uint8Array(text.length * 2);
|
|
63
|
+
for (let i = 0; i < text.length; i++) {
|
|
64
|
+
const code = text.charCodeAt(i);
|
|
65
|
+
out[i * 2] = code % 256;
|
|
66
|
+
out[i * 2 + 1] = Math.floor(code / 256);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function fromUtf16le(bytes) {
|
|
71
|
+
let out = "";
|
|
72
|
+
for (let i = 0; i + 1 < bytes.length; i += 2) {
|
|
73
|
+
out += globalThis.String.fromCharCode(bytes[i] + bytes[i + 1] * 256);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function concat(a, b) {
|
|
78
|
+
const out = new Uint8Array(a.length + b.length);
|
|
79
|
+
out.set(a, 0);
|
|
80
|
+
out.set(b, a.length);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
class DatasetStatements {
|
|
84
|
+
context;
|
|
85
|
+
constructor(context) {
|
|
86
|
+
this.context = context;
|
|
87
|
+
}
|
|
88
|
+
host(statement) {
|
|
89
|
+
if (this.context.dataset === undefined) {
|
|
90
|
+
throw new Error(`${statement}, not supported: no dataset host installed (abap.context.dataset)`);
|
|
91
|
+
}
|
|
92
|
+
return this.context.dataset;
|
|
93
|
+
}
|
|
94
|
+
opened(name) {
|
|
95
|
+
const file = this.context.datasets[name];
|
|
96
|
+
if (file === undefined) {
|
|
97
|
+
(0, throw_error_1.throwError)("CX_SY_FILE_OPEN_MODE");
|
|
98
|
+
}
|
|
99
|
+
return file;
|
|
100
|
+
}
|
|
101
|
+
async openDataset(nameIn, options) {
|
|
102
|
+
const host = this.host("OPEN DATASET");
|
|
103
|
+
if (options.legacy === true || options.encoding === "NON-UNICODE") {
|
|
104
|
+
throw new Error("OPEN DATASET, LEGACY and NON-UNICODE modes not supported");
|
|
105
|
+
}
|
|
106
|
+
if (options.unsupported !== undefined && options.unsupported.length > 0) {
|
|
107
|
+
throw new Error(`OPEN DATASET, not supported: ${options.unsupported.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
const name = nameOf(nameIn);
|
|
110
|
+
if (this.context.datasets[name] !== undefined) {
|
|
111
|
+
(0, throw_error_1.throwError)("CX_SY_FILE_OPEN");
|
|
112
|
+
}
|
|
113
|
+
const opened = await host.open(name, options.mode);
|
|
114
|
+
if (!("read" in opened)) {
|
|
115
|
+
if (options.message !== undefined) {
|
|
116
|
+
deref(options.message).set(opened.message);
|
|
117
|
+
}
|
|
118
|
+
setSubrc(8);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let position = 0;
|
|
122
|
+
if (options.mode === "APPENDING") {
|
|
123
|
+
position = await opened.size();
|
|
124
|
+
}
|
|
125
|
+
const at = numberOf(options.position);
|
|
126
|
+
if (at !== undefined) {
|
|
127
|
+
position = at;
|
|
128
|
+
}
|
|
129
|
+
this.context.datasets[name] = { handle: opened, mode: options.mode, binary: options.binary, position };
|
|
130
|
+
setSubrc(0);
|
|
131
|
+
}
|
|
132
|
+
async closeDataset(nameIn) {
|
|
133
|
+
this.host("CLOSE DATASET");
|
|
134
|
+
const name = nameOf(nameIn);
|
|
135
|
+
const file = this.context.datasets[name];
|
|
136
|
+
// closing a file that is not open is no error (measured: sy-subrc 0)
|
|
137
|
+
if (file !== undefined) {
|
|
138
|
+
delete this.context.datasets[name];
|
|
139
|
+
await file.handle.close();
|
|
140
|
+
}
|
|
141
|
+
setSubrc(0);
|
|
142
|
+
}
|
|
143
|
+
async deleteDataset(nameIn) {
|
|
144
|
+
const host = this.host("DELETE DATASET");
|
|
145
|
+
const name = nameOf(nameIn);
|
|
146
|
+
// deleting an open file answers 0 and closes it: a READ after it raises
|
|
147
|
+
// CX_SY_FILE_OPEN_MODE (measured)
|
|
148
|
+
const file = this.context.datasets[name];
|
|
149
|
+
if (file !== undefined) {
|
|
150
|
+
delete this.context.datasets[name];
|
|
151
|
+
await file.handle.close();
|
|
152
|
+
}
|
|
153
|
+
setSubrc(await host.delete(name) ? 0 : 4);
|
|
154
|
+
}
|
|
155
|
+
async transfer(sourceIn, nameIn, options = {}) {
|
|
156
|
+
this.host("TRANSFER");
|
|
157
|
+
const file = this.opened(nameOf(nameIn));
|
|
158
|
+
if (file.mode === "INPUT") {
|
|
159
|
+
(0, throw_error_1.throwError)("CX_SY_FILE_OPEN_MODE");
|
|
160
|
+
}
|
|
161
|
+
const source = deref(sourceIn);
|
|
162
|
+
const length = numberOf(options.length);
|
|
163
|
+
let bytes;
|
|
164
|
+
if (file.binary === true) {
|
|
165
|
+
if (isHex(source) || source instanceof types_1.XString) {
|
|
166
|
+
bytes = hexToBytes(source.get());
|
|
167
|
+
}
|
|
168
|
+
else if (isCharLike(source)) {
|
|
169
|
+
let text = source.get();
|
|
170
|
+
if (source instanceof types_1.Character) {
|
|
171
|
+
text = text.padEnd(source.getLength(), " ");
|
|
172
|
+
}
|
|
173
|
+
bytes = utf16le(length === undefined ? text : text.substring(0, length));
|
|
174
|
+
if (length !== undefined) {
|
|
175
|
+
const want = length * 2;
|
|
176
|
+
bytes = bytes.length >= want ? bytes : concat(bytes, utf16le(" ".repeat((want - bytes.length) / 2)));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
throw new Error("TRANSFER, BINARY MODE supports byte-like and character-like fields only");
|
|
181
|
+
}
|
|
182
|
+
if (length !== undefined && (isHex(source) || source instanceof types_1.XString)) {
|
|
183
|
+
bytes = bytes.length >= length ? bytes.slice(0, length) : concat(bytes, new Uint8Array(length - bytes.length));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
if (!isCharLike(source)) {
|
|
188
|
+
throw new Error("TRANSFER, TEXT MODE is only supported for character-type data objects");
|
|
189
|
+
}
|
|
190
|
+
let text = source.get();
|
|
191
|
+
if (length !== undefined) {
|
|
192
|
+
if (source instanceof types_1.Character) {
|
|
193
|
+
text = text.padEnd(source.getLength(), " ");
|
|
194
|
+
}
|
|
195
|
+
text = text.substring(0, length);
|
|
196
|
+
}
|
|
197
|
+
else if (source instanceof types_1.Character) {
|
|
198
|
+
// measured: c(10) 'ab' is written as 'ab' and a line end;
|
|
199
|
+
// a string keeps its trailing blanks
|
|
200
|
+
text = source.getTrimEnd();
|
|
201
|
+
}
|
|
202
|
+
if (options.noEndOfLine !== true) {
|
|
203
|
+
text = text + "\n";
|
|
204
|
+
}
|
|
205
|
+
bytes = new TextEncoder().encode(text);
|
|
206
|
+
}
|
|
207
|
+
await file.handle.write(file.position, bytes);
|
|
208
|
+
file.position += bytes.length;
|
|
209
|
+
setSubrc(0);
|
|
210
|
+
}
|
|
211
|
+
async readDataset(nameIn, targetIn, options = {}) {
|
|
212
|
+
this.host("READ DATASET");
|
|
213
|
+
const file = this.opened(nameOf(nameIn));
|
|
214
|
+
const target = deref(targetIn);
|
|
215
|
+
const maximum = numberOf(options.maximumLength);
|
|
216
|
+
const setActual = (n) => {
|
|
217
|
+
if (options.actualLength !== undefined) {
|
|
218
|
+
deref(options.actualLength).set(n);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
// a file opened for writing only answers 4 (measured, OUTPUT and APPENDING)
|
|
222
|
+
if (file.mode === "OUTPUT" || file.mode === "APPENDING") {
|
|
223
|
+
setActual(0);
|
|
224
|
+
setSubrc(4);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (file.binary === false) {
|
|
228
|
+
await this.readLine(file, target, maximum, setActual);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
await this.readBytes(file, target, maximum, setActual);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// TEXT MODE: one line per READ, without its LF (a CR before it stays,
|
|
235
|
+
// measured); the last line needs no LF; at the end sy-subrc 4 and the target
|
|
236
|
+
// cleared. ACTUAL LENGTH is the whole line even when the target is shorter,
|
|
237
|
+
// and the whole line is consumed either way.
|
|
238
|
+
async readLine(file, target, maximum, setActual) {
|
|
239
|
+
if (!isCharLike(target)) {
|
|
240
|
+
throw new Error("READ DATASET, the current statement is only supported for character-type data objects");
|
|
241
|
+
}
|
|
242
|
+
if (maximum !== undefined) {
|
|
243
|
+
throw new Error("READ DATASET, MAXIMUM LENGTH in TEXT MODE not supported");
|
|
244
|
+
}
|
|
245
|
+
let line = new Uint8Array(0);
|
|
246
|
+
let position = file.position;
|
|
247
|
+
let ended = false;
|
|
248
|
+
for (;;) {
|
|
249
|
+
const chunk = await file.handle.read(position, CHUNK);
|
|
250
|
+
if (chunk.length === 0) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
const lf = chunk.indexOf(0x0A);
|
|
254
|
+
if (lf >= 0) {
|
|
255
|
+
line = concat(line, chunk.subarray(0, lf));
|
|
256
|
+
position += lf + 1;
|
|
257
|
+
ended = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
line = concat(line, chunk);
|
|
261
|
+
position += chunk.length;
|
|
262
|
+
}
|
|
263
|
+
if (ended === false && line.length === 0) {
|
|
264
|
+
target.clear();
|
|
265
|
+
setActual(0);
|
|
266
|
+
setSubrc(4);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
file.position = position;
|
|
270
|
+
const text = new TextDecoder("utf-8").decode(line);
|
|
271
|
+
target.set(text);
|
|
272
|
+
setActual(text.length);
|
|
273
|
+
setSubrc(0);
|
|
274
|
+
}
|
|
275
|
+
// BINARY MODE: a field of fixed length takes its length in bytes (a C
|
|
276
|
+
// field two per character, UTF-16LE), a string or xstring the rest of the
|
|
277
|
+
// file; MAXIMUM LENGTH caps the bytes; fewer bytes than asked is sy-subrc 4
|
|
278
|
+
// and ACTUAL LENGTH counts bytes (measured)
|
|
279
|
+
async readBytes(file, target, maximum, setActual) {
|
|
280
|
+
let want;
|
|
281
|
+
if (isHex(target)) {
|
|
282
|
+
want = target.getLength();
|
|
283
|
+
}
|
|
284
|
+
else if (target instanceof types_1.Character) {
|
|
285
|
+
want = target.getLength() * 2;
|
|
286
|
+
}
|
|
287
|
+
else if (target instanceof types_1.XString || target instanceof types_1.String) {
|
|
288
|
+
want = Math.max(0, await file.handle.size() - file.position);
|
|
289
|
+
}
|
|
290
|
+
else if (isCharLike(target) && typeof target.getLength === "function") {
|
|
291
|
+
want = target.getLength() * 2;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
throw new Error("READ DATASET, BINARY MODE supports byte-like and character-like fields only");
|
|
295
|
+
}
|
|
296
|
+
const variable = target instanceof types_1.XString || target instanceof types_1.String;
|
|
297
|
+
if (maximum !== undefined) {
|
|
298
|
+
want = Math.min(want, maximum);
|
|
299
|
+
}
|
|
300
|
+
const bytes = want === 0 ? new Uint8Array(0) : await file.handle.read(file.position, want);
|
|
301
|
+
file.position += bytes.length;
|
|
302
|
+
if (isHex(target)) {
|
|
303
|
+
target.set(bytesToHex(bytes).padEnd(target.getLength() * 2, "0"));
|
|
304
|
+
}
|
|
305
|
+
else if (target instanceof types_1.XString) {
|
|
306
|
+
target.set(bytesToHex(bytes));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
target.set(fromUtf16le(bytes));
|
|
310
|
+
}
|
|
311
|
+
setActual(bytes.length);
|
|
312
|
+
if (variable === true) {
|
|
313
|
+
setSubrc(bytes.length > 0 ? 0 : 4);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
setSubrc(bytes.length < want || want === 0 ? 4 : 0);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async getDataset(nameIn, options) {
|
|
320
|
+
this.host("GET DATASET");
|
|
321
|
+
const file = this.opened(nameOf(nameIn));
|
|
322
|
+
if (options.attributes !== undefined) {
|
|
323
|
+
throw new Error("GET DATASET, ATTRIBUTES not supported");
|
|
324
|
+
}
|
|
325
|
+
if (options.position !== undefined) {
|
|
326
|
+
deref(options.position).set(file.position);
|
|
327
|
+
}
|
|
328
|
+
setSubrc(0);
|
|
329
|
+
}
|
|
330
|
+
async setDataset(nameIn, options) {
|
|
331
|
+
this.host("SET DATASET");
|
|
332
|
+
const file = this.opened(nameOf(nameIn));
|
|
333
|
+
if (options.endOfFile === true) {
|
|
334
|
+
file.position = await file.handle.size();
|
|
335
|
+
}
|
|
336
|
+
else if (options.position !== undefined) {
|
|
337
|
+
file.position = numberOf(options.position) ?? 0;
|
|
338
|
+
}
|
|
339
|
+
setSubrc(0);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
exports.DatasetStatements = DatasetStatements;
|
|
343
|
+
//# sourceMappingURL=dataset.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { append } from "./append";
|
|
2
|
+
import { IGetDatasetOptions, IOpenDatasetOptions, IReadDatasetOptions, ITransferOptions } from "../dataset/dataset";
|
|
2
3
|
import { assert } from "./assert";
|
|
3
4
|
import { assign } from "./assign";
|
|
4
5
|
import { ICommitOptions } from "./commit";
|
|
@@ -110,5 +111,15 @@ export declare class Statements {
|
|
|
110
111
|
message(options: IMessageOptions): Promise<void>;
|
|
111
112
|
readReport(name: ICharacter, options: Parameters<typeof readReport>[1]): Promise<void>;
|
|
112
113
|
write(source: INumeric | ICharacter | FieldSymbol | string | number, options?: IWriteOptions): void;
|
|
114
|
+
openDataset(name: any, options: IOpenDatasetOptions): Promise<void>;
|
|
115
|
+
closeDataset(name: any): Promise<void>;
|
|
116
|
+
deleteDataset(name: any): Promise<void>;
|
|
117
|
+
transfer(source: any, name: any, options?: ITransferOptions): Promise<void>;
|
|
118
|
+
readDataset(name: any, target: any, options?: IReadDatasetOptions): Promise<void>;
|
|
119
|
+
getDataset(name: any, options: IGetDatasetOptions): Promise<void>;
|
|
120
|
+
setDataset(name: any, options: {
|
|
121
|
+
position?: any;
|
|
122
|
+
endOfFile?: boolean;
|
|
123
|
+
}): Promise<void>;
|
|
113
124
|
skip(options?: ISkipOptions): void;
|
|
114
125
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Statements = void 0;
|
|
4
4
|
const append_1 = require("./append");
|
|
5
|
+
const dataset_1 = require("./dataset");
|
|
5
6
|
const assert_1 = require("./assert");
|
|
6
7
|
const assign_1 = require("./assign");
|
|
7
8
|
const commit_1 = require("./commit");
|
|
@@ -148,6 +149,27 @@ class Statements {
|
|
|
148
149
|
write(source, options) {
|
|
149
150
|
return new write_1.WriteStatement(this.context).write(source, options);
|
|
150
151
|
}
|
|
152
|
+
async openDataset(name, options) {
|
|
153
|
+
return new dataset_1.DatasetStatements(this.context).openDataset(name, options);
|
|
154
|
+
}
|
|
155
|
+
async closeDataset(name) {
|
|
156
|
+
return new dataset_1.DatasetStatements(this.context).closeDataset(name);
|
|
157
|
+
}
|
|
158
|
+
async deleteDataset(name) {
|
|
159
|
+
return new dataset_1.DatasetStatements(this.context).deleteDataset(name);
|
|
160
|
+
}
|
|
161
|
+
async transfer(source, name, options) {
|
|
162
|
+
return new dataset_1.DatasetStatements(this.context).transfer(source, name, options);
|
|
163
|
+
}
|
|
164
|
+
async readDataset(name, target, options) {
|
|
165
|
+
return new dataset_1.DatasetStatements(this.context).readDataset(name, target, options);
|
|
166
|
+
}
|
|
167
|
+
async getDataset(name, options) {
|
|
168
|
+
return new dataset_1.DatasetStatements(this.context).getDataset(name, options);
|
|
169
|
+
}
|
|
170
|
+
async setDataset(name, options) {
|
|
171
|
+
return new dataset_1.DatasetStatements(this.context).setDataset(name, options);
|
|
172
|
+
}
|
|
151
173
|
skip(options) {
|
|
152
174
|
return new skip_1.SkipStatement(this.context).skip(options);
|
|
153
175
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loop = loop;
|
|
4
4
|
const binary_search_1 = require("../binary_search");
|
|
5
|
+
const compare_1 = require("../compare");
|
|
5
6
|
const types_1 = require("../types");
|
|
6
7
|
function determineFromTo(array, topEquals, key) {
|
|
7
8
|
if (topEquals === undefined) {
|
|
@@ -27,6 +28,53 @@ function determineFromTo(array, topEquals, key) {
|
|
|
27
28
|
to: to,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
/** -1 / 0 / 1: the key field x sorts before / equal to / after the WHERE operand
|
|
32
|
+
* v - only for pairs whose order agrees with the table's sort (sort.ts compares
|
|
33
|
+
* the fields with lt/eq): the same type, of the same length for c, n and x, or
|
|
34
|
+
* a c operand against a string field, which eq compares by its getTrimEnd() */
|
|
35
|
+
function keyComparator(sample, value) {
|
|
36
|
+
if (value === null || typeof value !== "object" || sample === null || typeof sample !== "object") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (sample.constructor === value.constructor) {
|
|
40
|
+
const fixed = sample instanceof types_1.Character || sample instanceof types_1.Numc || sample instanceof types_1.Hex;
|
|
41
|
+
if (fixed && sample.getLength() !== value.getLength()) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return (x, v) => ((0, compare_1.eq)(x, v) ? 0 : (0, compare_1.lt)(x, v) ? -1 : 1);
|
|
45
|
+
}
|
|
46
|
+
if (sample instanceof types_1.String && value instanceof types_1.Character) {
|
|
47
|
+
return (x, v) => {
|
|
48
|
+
const s = v.getTrimEnd();
|
|
49
|
+
const xs = x.get();
|
|
50
|
+
return xs === s ? 0 : xs < s ? -1 : 1;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** A SORTED primary key, and a WHERE that requires its first field to equal a
|
|
56
|
+
* value: the rows the WHERE can accept are one block of the array. Relies on
|
|
57
|
+
* topEquals holding only conditions every row has to meet - see the
|
|
58
|
+
* transpiler's LoopTranspiler, which emits it for a conjunction only */
|
|
59
|
+
function sortedPrimaryBlock(table, options) {
|
|
60
|
+
const primary = table.getOptions()?.primaryKey;
|
|
61
|
+
if (!(table instanceof types_1.Table) || primary?.type !== types_1.TableAccessType.sorted
|
|
62
|
+
|| !primary.keyFields?.length || options?.topEquals === undefined) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const field = primary.keyFields[0].toLowerCase();
|
|
66
|
+
const value = options.topEquals[field];
|
|
67
|
+
const rowType = table.getRowType();
|
|
68
|
+
const structured = rowType instanceof types_1.Structure;
|
|
69
|
+
if (value === undefined || structured === (field === "table_line")) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const compare = keyComparator(structured ? rowType.get()[field] : rowType, value);
|
|
73
|
+
if (compare === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return structured ? (row) => compare(row.get()[field], value) : (row) => compare(row, value);
|
|
77
|
+
}
|
|
30
78
|
// todo: rewrite, this is a mess & hack & slow
|
|
31
79
|
function dynamicToWhere(condition, evaluate) {
|
|
32
80
|
// console.dir(condition);
|
|
@@ -103,6 +151,7 @@ async function* loop(table, options) {
|
|
|
103
151
|
let loopFrom = options?.from && options?.from.get() > 0 ? options.from.get() - 1 : 0;
|
|
104
152
|
let loopTo = options?.to && options.to.get() < length ? options.to.get() : length;
|
|
105
153
|
let array = [];
|
|
154
|
+
let block = undefined;
|
|
106
155
|
// a dynamic key name may come in any case, USING KEY ('PRIMARY_KEY') is the primary key too
|
|
107
156
|
const isPrimaryKey = options?.usingKey === undefined || options.usingKey.toLowerCase() === "primary_key";
|
|
108
157
|
if (options?.usingKey && isPrimaryKey === false) {
|
|
@@ -113,6 +162,24 @@ async function* loop(table, options) {
|
|
|
113
162
|
}
|
|
114
163
|
else {
|
|
115
164
|
array = table.array();
|
|
165
|
+
if (options?.where !== undefined && options.from === undefined && options.to === undefined) {
|
|
166
|
+
block = sortedPrimaryBlock(table, options);
|
|
167
|
+
}
|
|
168
|
+
if (block !== undefined) {
|
|
169
|
+
// the first row that does not sort before the value
|
|
170
|
+
let lo = 0;
|
|
171
|
+
let hi = array.length;
|
|
172
|
+
while (lo < hi) {
|
|
173
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
174
|
+
if (block(array[mid]) < 0) {
|
|
175
|
+
lo = mid + 1;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
hi = mid;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
loopFrom = lo;
|
|
182
|
+
}
|
|
116
183
|
}
|
|
117
184
|
const loopController = table.startLoop(loopFrom, loopTo, array);
|
|
118
185
|
let entered = false;
|
|
@@ -143,6 +210,10 @@ async function* loop(table, options) {
|
|
|
143
210
|
break;
|
|
144
211
|
}
|
|
145
212
|
const current = array[loopController.index];
|
|
213
|
+
if (block !== undefined && block(current) > 0) {
|
|
214
|
+
// this row sorts after the value, and so does every row behind it
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
146
217
|
if (options?.where) {
|
|
147
218
|
const row = isStructured ? current.get() : { table_line: current };
|
|
148
219
|
if (await options.where(row) === false) {
|