@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.
@@ -32,7 +32,7 @@ function find(input) {
32
32
  }
33
33
  }
34
34
  else {
35
- const sub = typeof input.sub === "string" ? input.sub : input.sub?.get();
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 || "", off);
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
@@ -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 pattern = "";
39
- for (let i = 0; i < r.length; i++) {
40
- const current = r[i];
41
- if (current === "#") {
42
- if (i + 1 < r.length) {
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
- const reg = new RegExp("^" + pattern + "$", "iu");
68
- return l.match(reg) !== null;
81
+ return reg.test(l);
69
82
  }
70
83
  //# sourceMappingURL=cp.js.map
@@ -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
  }
@@ -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,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=dataset.js.map
@@ -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
@@ -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: {
@@ -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
- const read_table_1 = require("./read_table");
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
- // with header line
11
- const read = (0, read_table_1.readTable)(source, { withKey: (i) => { return (0, compare_1.eq)(i.table_line, source.getHeader()); } });
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
- else if (target !== undefined) {
17
- const read = (0, read_table_1.readTable)(target, { withKey: (i) => { return (0, compare_1.eq)(i.table_line, source); } });
18
- if (read.subrc === 4) {
19
- (0, insert_internal_1.insertInternal)({ table: target, data: source });
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
- throw new Error("COLLECT, no target specified");
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 value = array[i].get();
42
- result += respectingBlanks === true ? value : value.trimEnd();
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
- let from = 0;
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, from, to, keyField, keyValue);
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
- if (options?.usingKey && options.usingKey !== undefined && options.usingKey !== "primary_key") {
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 || options.usingKey === "primary_key"
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
- if (loopController.index > array.length) {
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): TableRowType[];
100
+ getSecondaryIndex(name: string): any[];
101
101
  getQualifiedName(): string | undefined;
102
102
  getOptions(): ITableOptions;
103
103
  startLoop(from: number, to: number, array: any[]): LoopController;
@@ -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 || (lastComparison === 0 && unique === false)) {
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
- if (compare(this.value[middle], val) <= 0) {
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 > 0 && compare(this.value[low - 1], val) === 0) {
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.94",
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.2",
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.4"
41
+ "temporal-polyfill": "^1.0.5"
42
42
  }
43
43
  }