@maiyunnet/kebab 9.19.0 → 9.20.0

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.
@@ -226,6 +226,7 @@ export default class extends sCtr.Ctr {
226
226
  `<br><a href="${this._config.const.urlBase}test/sql?type=json">View "test/sql?type=json"</a> <a href="${this._config.const.urlBase}test/sql?type=json&s=pgsql">pgsql</a>`,
227
227
  `<br><a href="${this._config.const.urlBase}test/sql?type=having">View "test/sql?type=having"</a> <a href="${this._config.const.urlBase}test/sql?type=having&s=pgsql">pgsql</a>`,
228
228
  `<br><a href="${this._config.const.urlBase}test/sql?type=by">View "test/sql?type=by"</a> <a href="${this._config.const.urlBase}test/sql?type=by&s=pgsql">pgsql</a>`,
229
+ `<br><a href="${this._config.const.urlBase}test/sql?type=seek">View "test/sql?type=seek"</a> <a href="${this._config.const.urlBase}test/sql?type=seek&s=pgsql">pgsql</a>`,
229
230
  `<br><a href="${this._config.const.urlBase}test/sql?type=field">View "test/sql?type=field"</a> <a href="${this._config.const.urlBase}test/sql?type=field&s=pgsql">pgsql</a>`,
230
231
  `<br><a href="${this._config.const.urlBase}test/sql?type=hint">View "test/sql?type=hint"</a>`,
231
232
  '<br><br><b>Consistent:</b>',
@@ -803,6 +804,10 @@ test.set({
803
804
  });
804
805
  const result = await test.create();
805
806
  JSON.stringify(result));</pre>` + JSON.stringify(result));
807
+ if (result !== true) {
808
+ echo.push(`<p>The new test row was not inserted. The fixed test names and tokens may already be in use;
809
+ the cursor demo below reads existing test rows.</p>`);
810
+ }
806
811
  echo.push('<pre>JSON.stringify(test.toArray());</pre>' + lText.htmlescape(JSON.stringify(test.toArray())));
807
812
  echo.push('<br><br>Test table:');
808
813
  stmt = await db.query(this._get['s'] === 'pgsql' ?
@@ -836,6 +841,90 @@ const r = await ls.explain();</pre>` + lText.htmlescape(JSON.stringify(r)));
836
841
  else {
837
842
  echo.push('<div>false</div>');
838
843
  }
844
+ // --- 近似总数与游标分页,复用原始筛选,翻页不重复估算 ---
845
+ // --- 固定名称和 token 的测试数据可能已占满,不能依赖本次新增成功或只取最近五分钟 ---
846
+ const isPgsql = db.getService() === lDb.ESERVICE.PGSQL;
847
+ const query = mTest.select(db, [
848
+ // --- PG int8 可能超出 Number 精度,投影成文本,排序仍使用原表字段 ---
849
+ isPgsql ? 't.id::text id' : 't.id', 't.name', 't.time_add',
850
+ ], {
851
+ 'ctr': this,
852
+ 'pre': isPgsql ? 'm' : undefined,
853
+ 'alias': 't',
854
+ }).filter([
855
+ ['t.token', 'LIKE', 'test_%'],
856
+ ['t.time_add', '<=', time],
857
+ ]);
858
+ const estimated = await query.totalEstimate();
859
+ echo.push(`<br><b>Approximate total / cursor pagination:</b>
860
+ <p>totalEstimate() is PostgreSQL only; false means unavailable, not zero.
861
+ The estimate can be inaccurate; use hasMore to decide whether to fetch the next batch.
862
+ Keep filters and the ending time fixed while paging. Cursor fields must be non-null and jointly unique;
863
+ use a (time_add, id) index for large tables.
864
+ This demo includes existing test rows up to the fixed ending time, without a five-minute restriction.</p>
865
+ <pre>const isPgsql = db.getService() === lDb.ESERVICE.PGSQL;
866
+ const query = mTest.select&lt;mTest&gt;(db, [
867
+ isPgsql ? 't.id::text id' : 't.id', 't.name', 't.time_add',
868
+ ], {
869
+ 'ctr': this,
870
+ 'pre': isPgsql ? 'm' : undefined,
871
+ 'alias': 't',
872
+ }).filter([
873
+ ['t.token', 'LIKE', 'test_%'],
874
+ ['t.time_add', '&lt;=', time],
875
+ ]);
876
+ const estimated = await query.totalEstimate();</pre>
877
+ <b>estimated:</b> ${lText.htmlescape(lText.stringifyJson(estimated))}`);
878
+ const first = await query.allCursor(2, {
879
+ 'by': ['t.time_add', 't.id'],
880
+ 'order': 'DESC',
881
+ });
882
+ echo.push(`<pre>const first = await query.allCursor(2, {
883
+ 'by': ['t.time_add', 't.id'],
884
+ 'order': 'DESC',
885
+ });</pre><b>first:</b><pre>${lText.htmlescape(lText.stringifyJson(first, 4))}</pre>`);
886
+ if (first === false) {
887
+ echo.push('<p>First batch query failed.</p>');
888
+ }
889
+ else if (first.hasMore && first.next) {
890
+ const second = await query.allCursor(2, {
891
+ 'by': ['t.time_add', 't.id'],
892
+ 'order': 'DESC',
893
+ 'cursor': first.next,
894
+ });
895
+ echo.push(`<pre>if (first !== false &amp;&amp; first.hasMore &amp;&amp; first.next) {
896
+ const second = await query.allCursor(2, {
897
+ 'by': ['t.time_add', 't.id'],
898
+ 'order': 'DESC',
899
+ 'cursor': first.next,
900
+ });
901
+ }</pre><b>second:</b><pre>${lText.htmlescape(lText.stringifyJson(second, 4))}</pre>`);
902
+ if (second === false) {
903
+ echo.push('<p>Next batch query failed.</p>');
904
+ }
905
+ else if (second.previous) {
906
+ const previous = await query.allCursor(2, {
907
+ 'by': ['t.time_add', 't.id'],
908
+ 'order': 'DESC',
909
+ 'cursor': second.previous,
910
+ 'before': true,
911
+ });
912
+ echo.push(`<pre>if (second !== false &amp;&amp; second.previous) {
913
+ const previous = await query.allCursor(2, {
914
+ 'by': ['t.time_add', 't.id'],
915
+ 'order': 'DESC',
916
+ 'cursor': second.previous,
917
+ 'before': true,
918
+ });
919
+ }</pre><b>previous:</b><pre>${lText.htmlescape(lText.stringifyJson(previous, 4))}</pre>
920
+ <p>With before: true, rows stay in DESC display order;
921
+ hasMore now indicates whether there are more rows before this batch.</p>`);
922
+ }
923
+ }
924
+ else {
925
+ echo.push(`<p>No next batch. The first list already contains the complete result;
926
+ its length is the exact total for this query.</p>`);
927
+ }
839
928
  let ft = await mTest.one(db, [
840
929
  ['time_add', '>', '0']
841
930
  ], {
@@ -3233,6 +3322,69 @@ Result:<pre id="result">Nothing.</pre>`);
3233
3322
  <b>format() :</b> ${sql.format(s, sd)}`);
3234
3323
  break;
3235
3324
  }
3325
+ case 'seek': {
3326
+ echo.push(`<p><b>Without cursor values, seek() and by() generate the same first-batch SQL.</b>
3327
+ With cursor values, seek() adds a WHERE boundary and sets ORDER BY. Compare the two next-batch examples below:
3328
+ by() + OFFSET skips rows by position, while seek() continues after the preceding batch's last row.</p>
3329
+ <p>by() only sets ORDER BY;
3330
+ do not call by() again after seek(). Call seek() before limit().
3331
+ Cursor fields must be non-null and jointly unique. Pass large integer IDs as strings.</p>`);
3332
+ sql.select(['t.id', 't.time_add'], 'test t').where([
3333
+ ['t.time_add', '>=', 1700000000],
3334
+ ['t.time_add', '<=', 1700600000],
3335
+ ]);
3336
+ echo.push(`<pre>sql.select(['t.id', 't.time_add'], 'test t').where([
3337
+ ['t.time_add', '&gt;=', 1700000000],
3338
+ ['t.time_add', '&lt;=', 1700600000],
3339
+ ]);</pre>`);
3340
+ // --- 同一筛选条件,对比普通排序与游标定位 ---
3341
+ const sorted = sql.copy().by(['t.time_add', 't.id'], 'DESC').limit(2);
3342
+ echo.push(`<b>by(): sort only</b>
3343
+ <pre>const sorted = sql.copy().by(['t.time_add', 't.id'], 'DESC').limit(2);</pre>
3344
+ <b>getSql():</b> ${lText.htmlescape(sorted.getSql())}<br>
3345
+ <b>getData():</b><pre>${lText.htmlescape(lText.stringifyJson(sorted.getData(), 4))}</pre>
3346
+ <p>This always reads the first two rows of the current matching result.</p><hr>`);
3347
+ const offsetNext = sql.copy().by(['t.time_add', 't.id'], 'DESC').limit(2, 2);
3348
+ echo.push(`<b>by() + OFFSET: next batch by position</b>
3349
+ <pre>const offsetNext = sql.copy().by(['t.time_add', 't.id'], 'DESC').limit(2, 2);</pre>
3350
+ <b>getSql():</b> ${lText.htmlescape(offsetNext.getSql())}<br>
3351
+ <b>getData():</b><pre>${lText.htmlescape(lText.stringifyJson(offsetNext.getData(), 4))}</pre>
3352
+ <p>limit(offset, count) skips the first two matching rows. Larger offsets require skipping more rows;
3353
+ new rows inserted before that position can shift subsequent pages.</p><hr>`);
3354
+ const next = sql.copy().seek(['t.time_add', 't.id'], [1700500000, '9007199254740993']).limit(2);
3355
+ echo.push(`<b>seek() with a cursor: next batch after a row</b>
3356
+ <pre>const next = sql.copy().seek(
3357
+ ['t.time_add', 't.id'], [1700500000, '9007199254740993']
3358
+ ).limit(2);</pre>
3359
+ <b>getSql():</b> ${lText.htmlescape(next.getSql())}<br>
3360
+ <b>getData():</b><pre>${lText.htmlescape(lText.stringifyJson(next.getData(), 4))}</pre>
3361
+ <p>This adds a WHERE boundary based on the last row's time_add and id, with no OFFSET.
3362
+ Use the actual last row from the preceding batch as the cursor; the values here are illustrative.
3363
+ A matching composite index lets the database locate that boundary.</p><hr>`);
3364
+ const first = sql.copy().seek(['t.time_add', 't.id']).limit(2);
3365
+ echo.push(`<b>seek() without a cursor: same SQL as by() for the first batch</b>
3366
+ <pre>const first = sql.copy().seek(['t.time_add', 't.id']).limit(2);</pre>
3367
+ <b>getSql():</b> ${lText.htmlescape(first.getSql())}<br>
3368
+ <b>getData():</b><pre>${lText.htmlescape(lText.stringifyJson(first.getData(), 4))}</pre>
3369
+ <p>Without a cursor, seek() generates the same SQL as the by() first-batch example above.
3370
+ It is the cursor values in the preceding seek() example that add the WHERE boundary.</p><hr>`);
3371
+ const previous = sql.copy().seek(['t.time_add', 't.id'], [1700500000, '9007199254740993'], 'ASC').limit(2);
3372
+ echo.push(`<pre>const previous = sql.copy().seek(
3373
+ ['t.time_add', 't.id'], [1700500000, '9007199254740993'], 'ASC'
3374
+ ).limit(2);</pre>
3375
+ <b>getSql():</b> ${lText.htmlescape(previous.getSql())}<br>
3376
+ <b>getData():</b><pre>${lText.htmlescape(lText.stringifyJson(previous.getData(), 4))}</pre>
3377
+ <p>SQL seek() scans the previous batch in ASC order; reverse the rows for DESC display.
3378
+ Model allCursor(..., { before: true }) handles that reversal automatically.</p><hr>`);
3379
+ const unpaged = first.copy(undefined, { 'order': false, 'limit': false });
3380
+ echo.push(`<pre>const unpaged = first.copy(undefined, { 'order': false, 'limit': false });</pre>
3381
+ <b>getSql():</b> ${lText.htmlescape(unpaged.getSql())}<br>
3382
+ <b>getData():</b><pre>${lText.htmlescape(lText.stringifyJson(unpaged.getData(), 4))}</pre>
3383
+ <b>first remains:</b> ${lText.htmlescape(first.getSql())}<br><br>
3384
+ copy() removes the outer ordering and pagination without changing the original.<br>
3385
+ It keeps WHERE conditions, including any cursor boundary; estimate the base filter before adding a cursor.`);
3386
+ break;
3387
+ }
3236
3388
  case 'field': {
3237
3389
  echo.push(`<pre>sql.field('abc');</pre>` + sql.field('abc'));
3238
3390
  echo.push(`<pre>sql.field('abc', 'a_');</pre>` + sql.field('abc', 'a_'));
@@ -4512,7 +4664,17 @@ function doUpload() {
4512
4664
  */
4513
4665
  _getEnd() {
4514
4666
  const rt = this._getRunTime();
4515
- return 'Processed in ' + rt.toString() + ' second(s), ' + (Math.round(rt * 10000000) / 10000).toString() + 'ms, ' + (Math.round(this._getMemoryUsage() / 1024 * 100) / 100).toString() + ' K.<style>*{font-family:Consolas,"Courier New",Courier,FreeMono,monospace;line-height: 1.5;font-size:12px;}pre{padding:10px;background-color:rgba(0,0,0,.07);white-space:pre-wrap;word-break:break-all;}hr{margin:20px 0;border-color:#000;border-style:dashed;border-width:1px 0 0 0;}td,th{padding:5px;border:solid 1px #000;}</style><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">';
4667
+ return 'Processed in ' + rt.toString() + ' second(s), ' + (Math.round(rt * 10000000) / 10000).toString() + 'ms, ' + (Math.round(this._getMemoryUsage() / 1024 * 100) / 100).toString() + ` K.
4668
+ <style>
4669
+ body{margin:20px;font-family:system-ui,-apple-system,"Segoe UI","Noto Sans",sans-serif;font-size:16px;line-height:1.65;color:#1f2937;background:#fff;overflow-wrap:anywhere;}
4670
+ pre,code,kbd,samp{font-family:ui-monospace,"Cascadia Code","SFMono-Regular",Consolas,"Liberation Mono",monospace;font-size:15px;line-height:1.6;}
4671
+ pre{margin:16px 0;padding:14px 16px;border:1px solid #e5e7eb;border-radius:6px;background:#f3f4f6;white-space:pre-wrap;word-break:normal;}
4672
+ b,strong,th{font-weight:600;}
4673
+ input,button,select,textarea{font:inherit;}
4674
+ hr{margin:24px 0;border:0;border-top:1px solid #d1d5db;}
4675
+ td,th{padding:8px 10px;border:1px solid #d1d5db;}
4676
+ @media(max-width:600px){body{margin:12px;}pre{padding:12px;}}
4677
+ </style><meta name="viewport" content="width=device-width,initial-scale=1">`;
4516
4678
  }
4517
4679
  }
4518
4680
  // --- 下面是测试代码 ---