@abaplint/runtime 2.13.94 → 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/builtin/find.js +33 -2
- package/build/src/compare/cp.js +42 -29
- 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/concatenate.js +4 -2
- 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 +84 -5
- package/build/src/statements/update_database.js +14 -0
- package/build/src/types/table.d.ts +1 -1
- package/build/src/types/table.js +16 -6
- package/package.json +3 -3
|
@@ -32,7 +32,7 @@ function find(input) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
else {
|
|
35
|
-
|
|
35
|
+
let sub = typeof input.sub === "string" ? input.sub : input.sub?.get() || "";
|
|
36
36
|
let off = (0, _position_1.position)(input.off) || 0;
|
|
37
37
|
let occ = (0, _position_1.position)(input.occ);
|
|
38
38
|
if (occ === 0) {
|
|
@@ -41,6 +41,26 @@ function find(input) {
|
|
|
41
41
|
else if (occ === undefined) {
|
|
42
42
|
occ = 1;
|
|
43
43
|
}
|
|
44
|
+
const caseInput = typeof input.case === "string" ? input.case : input.case?.get();
|
|
45
|
+
if (caseInput !== undefined && caseInput !== "X") {
|
|
46
|
+
val = foldCase(val);
|
|
47
|
+
sub = foldCase(sub);
|
|
48
|
+
}
|
|
49
|
+
if (occ < 0 && sub !== "" && off === 0) {
|
|
50
|
+
// search from the right, on the string as it is, so a "sub" of any length
|
|
51
|
+
// matches and the offset needs no mapping back
|
|
52
|
+
let from = val.length - sub.length;
|
|
53
|
+
let found = -1;
|
|
54
|
+
for (let i = 0; i < Math.abs(occ); i++) {
|
|
55
|
+
found = from < 0 ? -1 : val.lastIndexOf(sub, from);
|
|
56
|
+
if (found < 0) {
|
|
57
|
+
return new types_1.Integer().set(-1);
|
|
58
|
+
}
|
|
59
|
+
from = found - 1;
|
|
60
|
+
}
|
|
61
|
+
return new types_1.Integer().set(found);
|
|
62
|
+
}
|
|
63
|
+
// a negative "occ" with an empty "sub" or an "off" still comes here, unchanged
|
|
44
64
|
let negative = false;
|
|
45
65
|
if (occ < 0) {
|
|
46
66
|
negative = true;
|
|
@@ -54,7 +74,7 @@ function find(input) {
|
|
|
54
74
|
}
|
|
55
75
|
let found = -1;
|
|
56
76
|
for (let i = 0; i < occ; i++) {
|
|
57
|
-
found = val.indexOf(sub
|
|
77
|
+
found = val.indexOf(sub, off);
|
|
58
78
|
if (found >= 0) {
|
|
59
79
|
off = found + 1;
|
|
60
80
|
}
|
|
@@ -65,4 +85,15 @@ function find(input) {
|
|
|
65
85
|
return new types_1.Integer().set(found);
|
|
66
86
|
}
|
|
67
87
|
}
|
|
88
|
+
// lower case one character at a time and keep a character whose lower case form
|
|
89
|
+
// has another length, so every offset stays valid - a plain toLowerCase( )
|
|
90
|
+
// turns e.g. "\u0130" into two code units
|
|
91
|
+
function foldCase(str) {
|
|
92
|
+
let ret = "";
|
|
93
|
+
for (const character of str) {
|
|
94
|
+
const lower = character.toLowerCase();
|
|
95
|
+
ret += lower.length === character.length ? lower : character;
|
|
96
|
+
}
|
|
97
|
+
return ret;
|
|
98
|
+
}
|
|
68
99
|
//# sourceMappingURL=find.js.map
|
package/build/src/compare/cp.js
CHANGED
|
@@ -5,6 +5,41 @@ const types_1 = require("../types");
|
|
|
5
5
|
function escapeRegExpCharacter(input) {
|
|
6
6
|
return input.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
7
7
|
}
|
|
8
|
+
const CACHE_SIZE = 1000;
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
/** `end` is the pattern's length after the last token that is not a `*`: what
|
|
11
|
+
* follows is a run of trailing `[\s\S]*`, which matches any rest of the
|
|
12
|
+
* string - so it goes, and so does the `$` that made the engine walk there */
|
|
13
|
+
function compile(r) {
|
|
14
|
+
let pattern = "";
|
|
15
|
+
let end = 0;
|
|
16
|
+
for (let i = 0; i < r.length; i++) {
|
|
17
|
+
const current = r[i];
|
|
18
|
+
if (current === "*") {
|
|
19
|
+
pattern += "[\\s\\S]*";
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (current === "#") {
|
|
23
|
+
if (i + 1 < r.length) {
|
|
24
|
+
const next = r[i + 1];
|
|
25
|
+
pattern += next === "#" ? "#" : escapeRegExpCharacter(next);
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
pattern += "#";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (current === "+") {
|
|
33
|
+
pattern += "[\\s\\S]";
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
pattern += escapeRegExpCharacter(current);
|
|
37
|
+
}
|
|
38
|
+
end = pattern.length;
|
|
39
|
+
}
|
|
40
|
+
const open = end < pattern.length;
|
|
41
|
+
return new RegExp("^" + pattern.slice(0, end) + (open ? "" : "$"), "iu");
|
|
42
|
+
}
|
|
8
43
|
function cp(left, right) {
|
|
9
44
|
let l = "";
|
|
10
45
|
if (typeof left === "number" || typeof left === "string") {
|
|
@@ -35,36 +70,14 @@ function cp(left, right) {
|
|
|
35
70
|
else {
|
|
36
71
|
r = right.get().toString().trimEnd();
|
|
37
72
|
}
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
const next = r[i + 1];
|
|
44
|
-
if (next === "#") {
|
|
45
|
-
pattern += "#";
|
|
46
|
-
i++;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
pattern += escapeRegExpCharacter(next);
|
|
50
|
-
i++;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
pattern += "#";
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
else if (current === "*") {
|
|
58
|
-
pattern += "[\\s\\S]*";
|
|
59
|
-
}
|
|
60
|
-
else if (current === "+") {
|
|
61
|
-
pattern += "[\\s\\S]";
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
pattern += escapeRegExpCharacter(current);
|
|
73
|
+
let reg = cache.get(r);
|
|
74
|
+
if (reg === undefined) {
|
|
75
|
+
reg = compile(r);
|
|
76
|
+
if (cache.size >= CACHE_SIZE) {
|
|
77
|
+
cache.delete(cache.keys().next().value);
|
|
65
78
|
}
|
|
79
|
+
cache.set(r, reg);
|
|
66
80
|
}
|
|
67
|
-
|
|
68
|
-
return l.match(reg) !== null;
|
|
81
|
+
return reg.test(l);
|
|
69
82
|
}
|
|
70
83
|
//# sourceMappingURL=cp.js.map
|
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
|
|
@@ -38,8 +38,10 @@ function concatenate(input) {
|
|
|
38
38
|
if (hasSeparator === true && i > 0) {
|
|
39
39
|
result += sep;
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
41
|
+
const row = array[i];
|
|
42
|
+
const value = row.get();
|
|
43
|
+
// Preserve trailing blanks in string rows even without RESPECTING BLANKS.
|
|
44
|
+
result += respectingBlanks === true || row instanceof types_1.String ? value : value.trimEnd();
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
setTarget(input.target, result);
|
|
@@ -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,19 +2,24 @@
|
|
|
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) {
|
|
8
9
|
// if there is no WHERE supplied, its using the sorting of the secondary key
|
|
9
10
|
return { from: 1, to: array.length };
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
+
// 1-based, as the branch without topEquals above answers: when the key's
|
|
13
|
+
// first field is not in topEquals (a key over a substructure component,
|
|
14
|
+
// s-x, is never put there) nothing is narrowed, and the loop must not start
|
|
15
|
+
// at row index -1
|
|
16
|
+
let from = 1;
|
|
12
17
|
let to = array.length;
|
|
13
18
|
// todo: multi field
|
|
14
19
|
const keyField = key.keyFields[0].toLowerCase();
|
|
15
20
|
const keyValue = topEquals[keyField];
|
|
16
21
|
if (keyField && keyValue) {
|
|
17
|
-
from = (0, binary_search_1.binarySearchFrom)(array,
|
|
22
|
+
from = (0, binary_search_1.binarySearchFrom)(array, 0, to, keyField, keyValue);
|
|
18
23
|
to = (0, binary_search_1.binarySearchTo)(array, from, to, keyField, keyValue);
|
|
19
24
|
// console.dir("from: " + from + ", to: " + to);
|
|
20
25
|
}
|
|
@@ -23,6 +28,53 @@ function determineFromTo(array, topEquals, key) {
|
|
|
23
28
|
to: to,
|
|
24
29
|
};
|
|
25
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
|
+
}
|
|
26
78
|
// todo: rewrite, this is a mess & hack & slow
|
|
27
79
|
function dynamicToWhere(condition, evaluate) {
|
|
28
80
|
// console.dir(condition);
|
|
@@ -99,7 +151,10 @@ async function* loop(table, options) {
|
|
|
99
151
|
let loopFrom = options?.from && options?.from.get() > 0 ? options.from.get() - 1 : 0;
|
|
100
152
|
let loopTo = options?.to && options.to.get() < length ? options.to.get() : length;
|
|
101
153
|
let array = [];
|
|
102
|
-
|
|
154
|
+
let block = undefined;
|
|
155
|
+
// a dynamic key name may come in any case, USING KEY ('PRIMARY_KEY') is the primary key too
|
|
156
|
+
const isPrimaryKey = options?.usingKey === undefined || options.usingKey.toLowerCase() === "primary_key";
|
|
157
|
+
if (options?.usingKey && isPrimaryKey === false) {
|
|
103
158
|
array = table.getSecondaryIndex(options.usingKey);
|
|
104
159
|
const { from, to } = determineFromTo(array, options.topEquals, table.getKeyByName(options.usingKey));
|
|
105
160
|
loopFrom = Math.max(loopFrom, from) - 1;
|
|
@@ -107,6 +162,24 @@ async function* loop(table, options) {
|
|
|
107
162
|
}
|
|
108
163
|
else {
|
|
109
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
|
+
}
|
|
110
183
|
}
|
|
111
184
|
const loopController = table.startLoop(loopFrom, loopTo, array);
|
|
112
185
|
let entered = false;
|
|
@@ -122,7 +195,7 @@ async function* loop(table, options) {
|
|
|
122
195
|
// The same goes for a loop that reads an index table through a hash
|
|
123
196
|
// secondary key. Handing out 1, 2, 3 there looks helpful and is a lie the
|
|
124
197
|
// caller cannot tell from the truth.
|
|
125
|
-
const usedKey = options?.usingKey === undefined ||
|
|
198
|
+
const usedKey = options?.usingKey === undefined || isPrimaryKey
|
|
126
199
|
? undefined
|
|
127
200
|
: table.getKeyByName(options.usingKey);
|
|
128
201
|
const hasRowNumber = usedKey !== undefined
|
|
@@ -131,10 +204,16 @@ async function* loop(table, options) {
|
|
|
131
204
|
try {
|
|
132
205
|
const isStructured = array[0] instanceof types_1.Structure;
|
|
133
206
|
while (loopController.index < loopController.loopTo) {
|
|
134
|
-
|
|
207
|
+
// the body may have deleted rows: never read past the end, where
|
|
208
|
+
// array[array.length] is undefined
|
|
209
|
+
if (loopController.index >= array.length) {
|
|
135
210
|
break;
|
|
136
211
|
}
|
|
137
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
|
+
}
|
|
138
217
|
if (options?.where) {
|
|
139
218
|
const row = isStructured ? current.get() : { table_line: current };
|
|
140
219
|
if (await options.where(row) === false) {
|
|
@@ -15,6 +15,20 @@ async function updateDatabase(table, options, context) {
|
|
|
15
15
|
if (typeof table !== "string") {
|
|
16
16
|
table = table.get();
|
|
17
17
|
}
|
|
18
|
+
if (options.table !== undefined) {
|
|
19
|
+
// row by row, as a work area each: sy-subrc 4 when a row found nothing
|
|
20
|
+
// to update, sy-dbcnt the rows updated
|
|
21
|
+
let subrc = 0;
|
|
22
|
+
let dbcnt = 0;
|
|
23
|
+
for (const row of options.table.array()) {
|
|
24
|
+
await updateDatabase(table, { from: row }, context);
|
|
25
|
+
subrc = Math.max(subrc, abap.builtin.sy.get().subrc.get());
|
|
26
|
+
dbcnt += abap.builtin.sy.get().dbcnt.get();
|
|
27
|
+
}
|
|
28
|
+
abap.builtin.sy.get().subrc.set(subrc);
|
|
29
|
+
abap.builtin.sy.get().dbcnt.set(dbcnt);
|
|
30
|
+
return subrc;
|
|
31
|
+
}
|
|
18
32
|
const tabl = abap.DDIC[table.toUpperCase()];
|
|
19
33
|
if (tabl === undefined) {
|
|
20
34
|
await (0, throw_error_1.throwErrorWithParameters)("CX_SY_DYNAMIC_OSQL_SEMANTICS", { sqlmsg: new types_1.String().set(`Table ${table} not found`) });
|
|
@@ -97,7 +97,7 @@ export declare class Table implements ITable {
|
|
|
97
97
|
clone(): Table;
|
|
98
98
|
getArrayLength(): number;
|
|
99
99
|
getKeyByName(name: string): ITableKey | undefined;
|
|
100
|
-
getSecondaryIndex(name: string):
|
|
100
|
+
getSecondaryIndex(name: string): any[];
|
|
101
101
|
getQualifiedName(): string | undefined;
|
|
102
102
|
getOptions(): ITableOptions;
|
|
103
103
|
startLoop(from: number, to: number, array: any[]): LoopController;
|
package/build/src/types/table.js
CHANGED
|
@@ -27,6 +27,15 @@ var TableKeyType;
|
|
|
27
27
|
TableKeyType["user"] = "USER";
|
|
28
28
|
TableKeyType["empty"] = "EMPTY";
|
|
29
29
|
})(TableKeyType || (exports.TableKeyType = TableKeyType = {}));
|
|
30
|
+
/* rows are handed to a sorted secondary key in table order, and a duplicate of a
|
|
31
|
+
* non-unique key goes in front of its equals, as INSERT ... INTO TABLE does: reversed
|
|
32
|
+
* before the stable sort, the later of two equal rows comes first */
|
|
33
|
+
function secondaryDuplicatesFirst(rows, key) {
|
|
34
|
+
if (key.type === TableAccessType.sorted && key.isUnique !== true) {
|
|
35
|
+
rows.reverse();
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
38
|
+
}
|
|
30
39
|
class LoopController {
|
|
31
40
|
index;
|
|
32
41
|
loopTo;
|
|
@@ -109,7 +118,7 @@ class HashedTable {
|
|
|
109
118
|
throw `Table, secondary key "${name}" not found`;
|
|
110
119
|
}
|
|
111
120
|
// note, array() already is a copy, so it can be used,
|
|
112
|
-
const copy = this.array();
|
|
121
|
+
const copy = secondaryDuplicatesFirst(this.array(), secondary);
|
|
113
122
|
(0, sort_1.sort)(copy, { by: secondary.keyFields.map(k => { return { component: k.toLowerCase() }; }) });
|
|
114
123
|
this.secondaryIndexes[name.toUpperCase()] = copy;
|
|
115
124
|
return copy;
|
|
@@ -365,7 +374,7 @@ class Table {
|
|
|
365
374
|
if (secondary === undefined) {
|
|
366
375
|
throw `Table, secondary key "${name}" not found`;
|
|
367
376
|
}
|
|
368
|
-
const copy = [...this.value];
|
|
377
|
+
const copy = secondaryDuplicatesFirst([...this.value], secondary);
|
|
369
378
|
(0, sort_1.sort)(copy, { by: secondary.keyFields.map(k => { return { component: k.toLowerCase() }; }), skipSortedCheck: true });
|
|
370
379
|
this.secondaryIndexes[name.toUpperCase()] = copy;
|
|
371
380
|
return copy;
|
|
@@ -482,24 +491,25 @@ class Table {
|
|
|
482
491
|
return { value: this.insertIndex(val, 0, true), subrc: 0 };
|
|
483
492
|
}
|
|
484
493
|
const lastComparison = compare(this.value[lastIndex], val);
|
|
485
|
-
if (lastComparison < 0
|
|
494
|
+
if (lastComparison < 0) {
|
|
486
495
|
return { value: this.insertIndex(val, this.value.length, true), subrc: 0 };
|
|
487
496
|
}
|
|
488
|
-
else if (lastComparison === 0) {
|
|
497
|
+
else if (lastComparison === 0 && unique === true) {
|
|
489
498
|
return { value: undefined, subrc: 4 };
|
|
490
499
|
}
|
|
491
500
|
let low = 0;
|
|
492
501
|
let high = this.value.length;
|
|
493
502
|
while (low < high) {
|
|
494
503
|
const middle = Math.floor((low + high) / 2);
|
|
495
|
-
|
|
504
|
+
// a duplicate of a non-unique key goes in front of the rows it duplicates
|
|
505
|
+
if (compare(this.value[middle], val) < 0) {
|
|
496
506
|
low = middle + 1;
|
|
497
507
|
}
|
|
498
508
|
else {
|
|
499
509
|
high = middle;
|
|
500
510
|
}
|
|
501
511
|
}
|
|
502
|
-
if (unique === true && low
|
|
512
|
+
if (unique === true && low < this.value.length && compare(this.value[low], val) === 0) {
|
|
503
513
|
return { value: undefined, subrc: 4 };
|
|
504
514
|
}
|
|
505
515
|
return { value: this.insertIndex(val, low, true), subrc: 0 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abaplint/runtime",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.96",
|
|
4
4
|
"description": "Transpiler - Runtime",
|
|
5
5
|
"main": "build/src/index.js",
|
|
6
6
|
"typings": "build/src/index.d.ts",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"@types/mocha": "^10.0.10",
|
|
34
34
|
"@types/node": "^26.4.1",
|
|
35
35
|
"chai": "^6.2.2",
|
|
36
|
-
"mocha": "^12.0.
|
|
36
|
+
"mocha": "^12.0.3",
|
|
37
37
|
"source-map-support": "^0.5.21",
|
|
38
38
|
"typescript": "^6.0.3"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"temporal-polyfill": "^1.0.
|
|
41
|
+
"temporal-polyfill": "^1.0.5"
|
|
42
42
|
}
|
|
43
43
|
}
|