@abaplint/runtime 2.13.94 → 2.13.95

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
@@ -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);
@@ -8,13 +8,17 @@ function determineFromTo(array, topEquals, key) {
8
8
  // if there is no WHERE supplied, its using the sorting of the secondary key
9
9
  return { from: 1, to: array.length };
10
10
  }
11
- let from = 0;
11
+ // 1-based, as the branch without topEquals above answers: when the key's
12
+ // first field is not in topEquals (a key over a substructure component,
13
+ // s-x, is never put there) nothing is narrowed, and the loop must not start
14
+ // at row index -1
15
+ let from = 1;
12
16
  let to = array.length;
13
17
  // todo: multi field
14
18
  const keyField = key.keyFields[0].toLowerCase();
15
19
  const keyValue = topEquals[keyField];
16
20
  if (keyField && keyValue) {
17
- from = (0, binary_search_1.binarySearchFrom)(array, from, to, keyField, keyValue);
21
+ from = (0, binary_search_1.binarySearchFrom)(array, 0, to, keyField, keyValue);
18
22
  to = (0, binary_search_1.binarySearchTo)(array, from, to, keyField, keyValue);
19
23
  // console.dir("from: " + from + ", to: " + to);
20
24
  }
@@ -99,7 +103,9 @@ async function* loop(table, options) {
99
103
  let loopFrom = options?.from && options?.from.get() > 0 ? options.from.get() - 1 : 0;
100
104
  let loopTo = options?.to && options.to.get() < length ? options.to.get() : length;
101
105
  let array = [];
102
- if (options?.usingKey && options.usingKey !== undefined && options.usingKey !== "primary_key") {
106
+ // a dynamic key name may come in any case, USING KEY ('PRIMARY_KEY') is the primary key too
107
+ const isPrimaryKey = options?.usingKey === undefined || options.usingKey.toLowerCase() === "primary_key";
108
+ if (options?.usingKey && isPrimaryKey === false) {
103
109
  array = table.getSecondaryIndex(options.usingKey);
104
110
  const { from, to } = determineFromTo(array, options.topEquals, table.getKeyByName(options.usingKey));
105
111
  loopFrom = Math.max(loopFrom, from) - 1;
@@ -122,7 +128,7 @@ async function* loop(table, options) {
122
128
  // The same goes for a loop that reads an index table through a hash
123
129
  // secondary key. Handing out 1, 2, 3 there looks helpful and is a lie the
124
130
  // caller cannot tell from the truth.
125
- const usedKey = options?.usingKey === undefined || options.usingKey === "primary_key"
131
+ const usedKey = options?.usingKey === undefined || isPrimaryKey
126
132
  ? undefined
127
133
  : table.getKeyByName(options.usingKey);
128
134
  const hasRowNumber = usedKey !== undefined
@@ -131,7 +137,9 @@ async function* loop(table, options) {
131
137
  try {
132
138
  const isStructured = array[0] instanceof types_1.Structure;
133
139
  while (loopController.index < loopController.loopTo) {
134
- if (loopController.index > array.length) {
140
+ // the body may have deleted rows: never read past the end, where
141
+ // array[array.length] is undefined
142
+ if (loopController.index >= array.length) {
135
143
  break;
136
144
  }
137
145
  const current = array[loopController.index];
@@ -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.95",
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
  }