jsql-neo 6.5.26 → 6.5.29

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
4
4
  > MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
5
5
 
6
- > **v6.5.26** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.29** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
@@ -171,6 +171,31 @@ await db.start();
171
171
 
172
172
  ---
173
173
 
174
+ ## Query results
175
+
176
+ `db.query()` / `executeSQL()` return **one result in three shapes**:
177
+
178
+ ```js
179
+ const r = await db.query('SELECT city, COUNT(*) AS c FROM users GROUP BY city');
180
+ r.columns // ['city', 'c']
181
+ r.rows // [['LA', 1], ['NY', 2]]
182
+ r.raw // [{ city: 'LA', c: 1 }, { city: 'NY', c: 2 }]
183
+ ```
184
+
185
+ `raw` is the object form of `rows`: its keys are exactly `columns`, and that
186
+ includes computed, aggregate and scalar-subquery columns. It is **not** the
187
+ pre-projection source row. For SELECT results `raw.length === rows.length`
188
+ always holds; non-SELECT statements may omit `raw`.
189
+
190
+ To check which native addon this installation is actually using:
191
+
192
+ - `jsql native status` — tag/version match (`0` = OK, `1` = mismatch or unverified)
193
+ - `jsql native verify` — re-hashes the `.node` on disk against the checksum recorded
194
+ at install time, so a swapped or corrupted binary is caught too. `--json` prints
195
+ a structured result for CI. States: `OK` / `MISMATCH` / `CORRUPT` / `MISSING` /
196
+ `UNVERIFIED` / `UNREADABLE`.
197
+ - `jsql native refresh` — re-fetches a checksum-verified addon
198
+
174
199
  ## ✨ XQL 集成
175
200
 
176
201
  用 [XQL](https://github.com/vexify-org/XQL) 查询 JSQL 表:把 SQL 直接接到文档上,
package/bin/jsql CHANGED
@@ -531,6 +531,38 @@ const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
531
531
  .command('update', 'Update jsql-neo to the latest version', updateOptions, updateHandler)
532
532
  .command('uninstall', 'Remove jsql user data and/or the global package', uninstallOptions, uninstallHandler)
533
533
  .command('remove', 'Alias of uninstall', uninstallOptions, uninstallHandler)
534
+ .command('native', 'Inspect or refresh the Rust native addon', (sub) => {
535
+ sub.option('json', { type: 'boolean', description: 'Machine-readable output (verify only)' });
536
+ }, async (argv) => {
537
+ const action = argv._[0] || 'status';
538
+ const pi = require('../postinstall.js');
539
+ if (action === 'verify') {
540
+ // 内容级:重算 .node 的 SHA256 与安装记录比对,status 只看 tag 发现不了替换/损坏
541
+ const r = pi.verifyNative();
542
+ if (argv.json) {
543
+ console.log(JSON.stringify(r, null, 2));
544
+ } else {
545
+ console.log(`[jsql-neo] VERIFY : ${r.state}`);
546
+ console.log(`[jsql-neo] tag : ${r.installedTag || 'unknown'} (expect ${r.expectedTag})`);
547
+ console.log(`[jsql-neo] sha256 : ${r.actualSha256 || 'n/a'}`);
548
+ if (r.state !== 'OK') console.log('[jsql-neo] refresh with: jsql native refresh');
549
+ }
550
+ process.exitCode = r.state === 'OK' ? 0 : 1;
551
+ return;
552
+ }
553
+ if (action === 'status') {
554
+ // 0 = 当前 .node 与包版本匹配;1 = 错配 / 未记录(可直接当 CI 门禁)
555
+ process.exitCode = pi.nativeStatus();
556
+ return;
557
+ }
558
+ if (action === 'refresh') {
559
+ await pi.ensureNativeNode();
560
+ process.exitCode = pi.nativeStatus();
561
+ return;
562
+ }
563
+ console.error(`Unknown native action: ${action}. Usage: jsql native <status|refresh|verify>`);
564
+ process.exitCode = 1;
565
+ })
534
566
  .command('version', 'Show version', null, () => {
535
567
  console.log(require('../package.json').version);
536
568
  })
package/index.d.ts CHANGED
@@ -272,9 +272,16 @@ declare namespace JSQLNeo {
272
272
  ok: boolean;
273
273
  type: string;
274
274
  table?: string | null;
275
+ /** 投影列名;rows[i][j] 与 columns[j] 一一对应。 */
275
276
  columns?: string[];
277
+ /** 投影后的二维数组形式。 */
276
278
  rows?: unknown[][];
277
- raw?: unknown[];
279
+ /**
280
+ * 与 rows 指向**同一份结果**的对象形式:raw[i] 的键恰好是 columns,
281
+ * 值取自 rows[i]。聚合 / 计算列 / 标量子查询都包含在内(issue #43)。
282
+ * 非 SELECT 语句可能没有此字段。
283
+ */
284
+ raw?: Array<Record<string, unknown>>;
278
285
  affectedRows?: number;
279
286
  insertId?: number | null;
280
287
  ids?: unknown[];
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.26 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.29 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -153,6 +153,10 @@ module.exports = {
153
153
  SQL: sql,
154
154
  executeSQL: sql.executeSQL,
155
155
  parseSQL: sql.parseSQL,
156
+ // Issue #46:index.d.ts 早就声明了这三个顶层导出,index.js 却一直没给
157
+ applyParams: sql.applyParams,
158
+ escapeValue: sql.escapeValue,
159
+ escapeId: sql.escapeId,
156
160
  // AST 访问与改写(5.5.0+)
157
161
  AST: sql.AST,
158
162
  walk: sql.walk,
package/lib/ast.js CHANGED
@@ -128,40 +128,47 @@ function transform(ast, fn) {
128
128
  return visit(ast);
129
129
  }
130
130
 
131
- /** 原地改写:fn(node) 返回值非 undefined 时替换原节点。 */
131
+ /**
132
+ * 原地改写:fn(node) 返回值非 undefined 时替换原节点。
133
+ *
134
+ * 根节点同样可替换(#58):此前 visit 只在 `parent && key` 时才调用 fn 并回写,
135
+ * 根节点 parent===null 一律被跳过,于是 rewrite 无法替换整棵树,而 transform 可以。
136
+ * 现在数组容器本身作为父级传入(数组元素按 index 回写),根节点替换通过
137
+ * 返回值交回调用方。
138
+ */
132
139
  function rewrite(ast, fn) {
133
140
  const seen = new Set();
141
+ let root = ast;
142
+
143
+ const assign = (parent, key, value) => {
144
+ if (parent === null || parent === undefined) { root = value; return; }
145
+ parent[key] = value;
146
+ };
134
147
 
135
- const visit = (value, parent, key, index) => {
148
+ const visit = (value, parent, key) => {
136
149
  if (value === null || typeof value !== 'object') {
137
- if (parent && key) {
138
- const r = fn(value);
139
- if (r !== undefined) {
140
- if (index === undefined) parent[key] = r; else parent[key][index] = r;
141
- }
142
- }
150
+ const r = fn(value);
151
+ if (r !== undefined) assign(parent, key, r);
143
152
  return;
144
153
  }
145
154
  if (seen.has(value)) return;
146
155
  seen.add(value);
147
156
 
148
157
  if (Array.isArray(value)) {
149
- for (let i = 0; i < value.length; i++) visit(value[i], parent, key, i);
158
+ for (let i = 0; i < value.length; i++) visit(value[i], value, i);
150
159
  return;
151
160
  }
152
161
 
153
162
  for (const k of childKeys(value)) visit(value[k], value, k);
154
163
 
155
- if (isNode(value) && parent && key) {
164
+ if (isNode(value)) {
156
165
  const r = fn(value);
157
- if (r !== undefined) {
158
- if (index === undefined) parent[key] = r; else parent[key][index] = r;
159
- }
166
+ if (r !== undefined) assign(parent, key, r);
160
167
  }
161
168
  };
162
169
 
163
170
  visit(ast, null, null);
164
- return ast;
171
+ return root;
165
172
  }
166
173
 
167
174
  /** 语句引用的所有表名(含 JOIN / 子查询)。 */
package/lib/btree.js CHANGED
@@ -296,31 +296,37 @@ class BTree {
296
296
  remove(key, rowIndex) {
297
297
  if (!this._root) return false;
298
298
  const removed = this._removeFromNode(this._root, key, rowIndex);
299
- // 删掉整个键(而非仅删某个 rowIndex)后,叶子的键域可能缩小,
300
- // 沿途的分隔键副本可能已指向不存在的键 —— 自顶向下重建一次。
301
- if (removed) {
299
+ // 返回值区分三种情况:
300
+ // 0 → 什么都没删(key 或 rowIndex 不存在)
301
+ // 1 / 'key'→ 整个键被删掉(该 key 的最后一个 rowIndex)
302
+ // 2 / 'row'→ 只删掉该 key 下的某一行,键仍在(#53)
303
+ // size 统计的是 distinct keys,所以只有「键消失」时才递减。
304
+ if (removed === 2) {
305
+ // 键仍存在,size 不变;分隔键也无需重建
306
+ } else if (removed) {
302
307
  this._size--;
303
308
  if (!this._root.leaf) this._fixSeparators(this._root);
304
309
  }
305
310
  if (this._root.keys.length === 0 && !this._root.leaf && this._root.children.length > 0) {
306
311
  this._root = this._root.children[0];
307
312
  }
308
- return removed;
313
+ // 对外契约是布尔值:删掉了行就是 true(此前重复 key 场景错误地返回 false)
314
+ return !!removed;
309
315
  }
310
316
 
311
317
  _removeFromNode(node, key, rowIndex) {
312
318
  if (node.leaf) {
313
319
  const i = this._lowerBound(node.keys, key);
314
- if (i >= node.keys.length || node.keys[i] !== key) return false;
315
- if (!node.values[i].includes(rowIndex)) return false;
320
+ if (i >= node.keys.length || node.keys[i] !== key) return 0;
321
+ if (!node.values[i].includes(rowIndex)) return 0;
316
322
  const vals = node.values[i].filter(v => v !== rowIndex);
317
323
  if (vals.length === 0) {
318
324
  node.keys.splice(i, 1);
319
325
  node.values.splice(i, 1);
320
- return true;
326
+ return 1;
321
327
  }
322
328
  node.values[i] = vals;
323
- return false;
329
+ return 2;
324
330
  }
325
331
 
326
332
  let i = this._route(node, key);
package/lib/database.js CHANGED
@@ -1837,7 +1837,11 @@ class Database {
1837
1837
 
1838
1838
  _resolveId(table, id) {
1839
1839
  const pk = this._pkOf(table);
1840
- if (pk) return table._rows.find(r => r[pk] === id) || null;
1840
+ if (pk) {
1841
+ // Issue #44:主键查询值按列类型归一('5' → 5),与索引/insert 保持一致
1842
+ const key = typeof table._coerceQueryValue === 'function' ? table._coerceQueryValue(pk, id) : id;
1843
+ return table._rows.find(r => r[pk] === key || r[pk] === id) || null;
1844
+ }
1841
1845
  // 无主键表:id 是行对象(可能是 find() 返回的浅拷贝)时,用内容匹配代替引用比较。
1842
1846
  // 做法:逐行比对所有字段值是否相等;是数字时当 1-based 行号。
1843
1847
  if (id && typeof id === 'object') {
package/lib/date-types.js CHANGED
@@ -35,6 +35,25 @@ function validateDateType(value, type, fieldName) {
35
35
 
36
36
  function pad2(n) { return String(n).padStart(2, '0'); }
37
37
 
38
+ /**
39
+ * 解析 ISO-8601 字符串尾部残余部分里的时区标记,返回「相对 UTC 的分钟偏移」。
40
+ * 输入形如 'Z'、'z'、'+08:00'、'-0500'、'.500Z'、' +02:00'。
41
+ * 没有时区标记(或无法识别)返回 null —— 调用方按本地时间字面值处理。
42
+ */
43
+ function parseIsoTimezone(rest) {
44
+ let s = String(rest == null ? '' : rest).trim();
45
+ // 去掉小数秒
46
+ if (s.startsWith('.')) s = s.replace(/^\.\d+/, '');
47
+ if (s === 'Z' || s === 'z') return 0;
48
+ const m = s.match(/^([+-])(\d{2}):?(\d{2})$/);
49
+ if (!m) return null;
50
+ const hh = parseInt(m[2], 10);
51
+ const mm = parseInt(m[3], 10);
52
+ if (hh > 23 || mm > 59) return null; // 非法偏移:交给后续校验报错
53
+ const sign = m[1] === '-' ? -1 : 1;
54
+ return sign * (hh * 60 + mm);
55
+ }
56
+
38
57
  /**
39
58
  * 本地时区的 'YYYY-MM-DD'。
40
59
  * 不能用 toISOString().slice(0,10) —— 那是 UTC:东八区 2023-01-01 00:00 本地
@@ -115,7 +134,7 @@ function validateDateTime(value, fieldName) {
115
134
  return toLocalDateTimeString(new Date(value * 1000));
116
135
  }
117
136
  if (typeof value === 'string') {
118
- // ISO format: 2024-01-15T10:30:00.000Z(带时区标记,按字面取日期+时间)
137
+ // ISO format: 2024-01-15T10:30:00.000Z / +08:00(可能带时区标记)
119
138
  const isoMatch = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
120
139
  if (isoMatch) {
121
140
  // P2-35: 验证日期和时间有效性
@@ -129,6 +148,15 @@ function validateDateTime(value, fieldName) {
129
148
  const dt = new Date(y, mo - 1, d);
130
149
  if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d &&
131
150
  h >= 0 && h < 24 && mi >= 0 && mi < 60 && s >= 0 && s < 60) {
151
+ // #50:带 Z / ±HH:MM 时区标记时必须换算到本地时区。
152
+ // 此前无条件按字面值存储,'2024-01-15T10:30:00Z'(UTC)在 UTC+8
153
+ // 会被存成本地 10:30,整体偏 8 小时。
154
+ const tz = parseIsoTimezone(value.slice(isoMatch[0].length));
155
+ if (tz !== null) {
156
+ const ms = Date.UTC(y, mo - 1, d, h, mi, s) - tz * 60000;
157
+ return toLocalDateTimeString(new Date(ms));
158
+ }
159
+ // 无时区标记:按本地时间字面值(MySQL 语义)
132
160
  return datePart + ' ' + timePart;
133
161
  }
134
162
  }
@@ -430,9 +430,16 @@ class MongoServer {
430
430
  for (const d of docs) {
431
431
  const doc2 = { ...d };
432
432
  // 检查 _id 唯一性
433
+ // Issue #44:索引查找是严格相等的,空结果不能直接当「不存在」——
434
+ // 查询值与已存值的类型不一致时会漏判。这里再加一道按 schema 归一的直接扫描。
433
435
  if (doc2._id !== undefined) {
434
436
  const existing = await engine.find(coll, { _id: doc2._id });
435
437
  if (existing.length > 0) continue; // 跳过重复 _id
438
+ const table = typeof engine._ensureTable === 'function' ? engine._ensureTable(coll) : null;
439
+ if (table && Array.isArray(table._rows) && typeof table._coerceQueryValue === 'function') {
440
+ const idKey = table._coerceQueryValue('_id', doc2._id);
441
+ if (table._rows.some((r) => r && !r._deleted && r._id === idKey)) continue;
442
+ }
436
443
  }
437
444
  await engine.insert(coll, doc2);
438
445
  ids.push(doc2._id);
@@ -568,10 +575,13 @@ class MongoServer {
568
575
  for (const [k, dir] of sortKeys) {
569
576
  const av = a[k]; const bv = b[k];
570
577
  if (av === bv) continue;
571
- if (av == null) return 1;
572
- if (bv == null) return -1;
578
+ const desc = Number(dir) < 0;
579
+ // Issue #47:NULL 的位置随排序方向走(升序在前、降序在后),
580
+ // 与 MongoDB 一致;此前固定返回 1/-1,升序时 null 恒排最后。
581
+ if (av == null) return desc ? 1 : -1;
582
+ if (bv == null) return desc ? -1 : 1;
573
583
  const cmp = av < bv ? -1 : 1;
574
- return Number(dir) < 0 ? -cmp : cmp;
584
+ return desc ? -cmp : cmp;
575
585
  }
576
586
  return 0;
577
587
  });
@@ -643,8 +653,14 @@ class MongoServer {
643
653
  const sum = grow.reduce((s, r) => s + (Number(r[field]) || 0), 0);
644
654
  doc[k] = (doc[k] || 0) + sum;
645
655
  } else if (v.$avg !== undefined) {
646
- const vals = grow.map(r => Number(r[field]) || 0);
647
- doc[k] = vals.reduce((a, b) => a + b, 0) / vals.length;
656
+ // Issue #45:null/undefined 不计入分子的同时也不计入分母;
657
+ // 全空组返回 null(MongoDB 语义),而不是 0。
658
+ const vals = grow
659
+ .map(r => r[field])
660
+ .filter(x => x !== null && x !== undefined)
661
+ .map(Number)
662
+ .filter(x => !Number.isNaN(x));
663
+ doc[k] = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
648
664
  } else if (v.$min !== undefined) {
649
665
  const vals = grow.map(r => r[field]).filter(v => v != null);
650
666
  let best = null;
@@ -20,7 +20,7 @@ function toResultPacket(r, table, rowsAsArray) {
20
20
  const fields = columns.map(name => {
21
21
  let type = 253; // VAR_STRING
22
22
  const val = rawRows[0] ? rawRows[0][columns.indexOf(name)] : null;
23
- if (typeof val === 'number') type = 8; // LONGLONG
23
+ if (typeof val === 'number') type = Number.isInteger(val) ? 8 : 5; // LONGLONG / DOUBLE(Issue #48:浮点列曾被误报为 8)
24
24
  else if (typeof val === 'boolean') type = 1; // TINY
25
25
  else if (val instanceof Date) type = 12; // DATETIME
26
26
  else if (Buffer.isBuffer(val)) type = 252; // BLOB
package/lib/query.js CHANGED
@@ -6,6 +6,36 @@
6
6
 
7
7
  const { createError } = require('./errors');
8
8
 
9
+ /**
10
+ * 稳定序列化(#55):把查询参数变成可比较的缓存键。
11
+ * 普通对象的键排序,函数/RegExp/BigInt/Date 保留可辨识信息,循环引用截断。
12
+ */
13
+ function stableKey(value, seen) {
14
+ seen = seen || new Set();
15
+ if (value === null) return 'null';
16
+ const t = typeof value;
17
+ if (t === 'bigint') return value.toString() + 'n';
18
+ if (t === 'number') return Object.is(value, -0) ? '-0' : String(value);
19
+ if (t === 'string') return JSON.stringify(value);
20
+ if (t === 'boolean') return String(value);
21
+ if (t === 'undefined') return 'undefined';
22
+ if (t === 'function') return 'fn:' + String(value);
23
+ if (t === 'symbol') return 'sym:' + String(value);
24
+ if (value instanceof RegExp) return 're:' + value.source + '/' + value.flags;
25
+ if (value instanceof Date) return 'date:' + value.getTime();
26
+ if (seen.has(value)) return '[circular]';
27
+ seen.add(value);
28
+ let out;
29
+ if (Array.isArray(value)) {
30
+ out = '[' + value.map(v => stableKey(v, seen)).join(',') + ']';
31
+ } else {
32
+ out = '{' + Object.keys(value).sort()
33
+ .map(k => JSON.stringify(k) + ':' + stableKey(value[k], seen)).join(',') + '}';
34
+ }
35
+ seen.delete(value);
36
+ return out;
37
+ }
38
+
9
39
  class Query {
10
40
  constructor(table, conditions = {}) {
11
41
  this._table = table;
@@ -210,12 +240,46 @@ class Query {
210
240
  // 缓存
211
241
  // ============================================================
212
242
 
243
+ /**
244
+ * 缓存键(#55):必须覆盖所有影响结果的参数。
245
+ * 旧实现只按 TTL 判断,同一 Query 实例换 where/orderBy/limit/offset 后
246
+ * 仍命中旧缓存,返回过期结果。
247
+ */
248
+ _cacheKey(depth = 0) {
249
+ if (depth > 5) return '[deep]';
250
+ const joins = this._joins.map(j => ({
251
+ type: j.type,
252
+ table: j.table && (j.table._name || j.table.name) || j.table,
253
+ localField: j.localField,
254
+ foreignField: j.foreignField,
255
+ as: j.as,
256
+ }));
257
+ return stableKey({
258
+ table: this._table && this._table._name,
259
+ conditions: this._conditions,
260
+ orderBy: this._orderBy,
261
+ limit: this._limitCount,
262
+ offset: this._offsetCount,
263
+ select: this._selectFields,
264
+ groupBy: this._groupBy,
265
+ having: this._having,
266
+ distinct: this._distinct,
267
+ distinctField: this._distinctField,
268
+ joins,
269
+ union: this._union ? this._union._cacheKey(depth + 1) : null,
270
+ caseExpr: this._caseExpr,
271
+ caseAs: this._caseAs,
272
+ window: this._window,
273
+ forUpdate: this._forUpdate,
274
+ });
275
+ }
276
+
213
277
  /**
214
278
  * 缓存查询结果
215
279
  * @param {number} ttlMs - 缓存有效期(毫秒),默认 60000
216
280
  */
217
281
  cache(ttlMs = 60000) {
218
- this._cache = { ttl: ttlMs, result: null, timestamp: 0 };
282
+ this._cache = { ttl: ttlMs, result: null, timestamp: 0, key: null };
219
283
  return this;
220
284
  }
221
285
 
@@ -227,8 +291,11 @@ class Query {
227
291
  const startTime = Date.now();
228
292
  if (this._explain) return this._getExplain();
229
293
 
294
+ // 缓存键要包含 where/orderBy/limit/offset 等全部参数(#55)
295
+ const cacheKey = this._cache ? this._cacheKey() : null;
230
296
  // 缓存命中: 返回每行的浅拷贝,与未命中路径一致,避免调用方修改污染缓存
231
- if (this._cache && this._cache.result && (Date.now() - this._cache.timestamp < this._cache.ttl)) {
297
+ if (this._cache && this._cache.result && this._cache.key === cacheKey &&
298
+ (Date.now() - this._cache.timestamp < this._cache.ttl)) {
232
299
  // #37:命中返回副本,而不是内部数组引用——调用方对返回值做变更
233
300
  // (push/splice/改字段)会污染缓存;未命中路径返回的是新数组,行为应一致。
234
301
  return this._cache.result.map(row => (row && typeof row === 'object' ? { ...row } : row));
@@ -334,6 +401,7 @@ class Query {
334
401
  if (this._cache) {
335
402
  this._cache.result = rows.map(row => (row && typeof row === 'object' ? { ...row } : row));
336
403
  this._cache.timestamp = Date.now();
404
+ this._cache.key = cacheKey;
337
405
  }
338
406
 
339
407
  this._table._db._logSlowQuery(`get ${this._table._name}`, Date.now() - startTime, rows.length);
@@ -428,6 +496,7 @@ class Query {
428
496
  if (this._cache) {
429
497
  this._cache.result = null;
430
498
  this._cache.timestamp = 0;
499
+ this._cache.key = null;
431
500
  }
432
501
  return this;
433
502
  }