pivotgrid-js 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pivotgrid-js",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Vanilla JS pivot table — no dependencies, no frameworks",
5
5
  "author": "Aleksandr Korolev <korolevalexa@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -271,19 +271,29 @@ class RestProvider {
271
271
  // ── HTTP ───────────────────────────────────────────────────────────────────
272
272
 
273
273
  async _execute(query) {
274
- const res = await fetch(this.url, {
275
- method: 'POST',
276
- headers: { 'Content-Type': 'application/json' },
277
- body: JSON.stringify({ query }),
278
- });
274
+ let page = 0;
275
+ let allRows = [];
276
+ let hasMore = true;
277
+
278
+ while (hasMore) {
279
+ const res = await fetch(this.url, {
280
+ method: 'POST',
281
+ headers: { 'Content-Type': 'application/json' },
282
+ body: JSON.stringify({ query, page }),
283
+ });
284
+
285
+ if (!res.ok) {
286
+ const err = await res.json().catch(() => ({}));
287
+ throw new Error(`Server error ${res.status}: ${err.error || ''}`);
288
+ }
279
289
 
280
- if (!res.ok) {
281
- const err = await res.json().catch(() => ({}));
282
- throw new Error(`Server error ${res.status}: ${err.error || ''}`);
290
+ const data = await res.json();
291
+ allRows = allRows.concat(data.rows);
292
+ hasMore = data.hasMore;
293
+ page++;
283
294
  }
284
295
 
285
- const rows = await res.json();
286
- return rows.map(row => {
296
+ return allRows.map(row => {
287
297
  const out = {};
288
298
  for (const k of Object.keys(row)) out[k.toLowerCase()] = row[k];
289
299
  return out;
package/server/README.md CHANGED
@@ -42,7 +42,7 @@ All settings can also be configured via the Config Editor UI.
42
42
 
43
43
  | Method | Path | Description |
44
44
  |--------|------|-------------|
45
- | `POST` | `/query` | Execute a SELECT query, returns JSON array |
45
+ | `POST` | `/query` | Execute a SELECT query, returns paginated JSON (`{rows, total, page, hasMore}`) |
46
46
  | `GET` | `/configs` | List all config names |
47
47
  | `GET` | `/configs/{name}` | Get a config by name |
48
48
  | `POST` | `/configs/{name}` | Save a config by name |
@@ -50,6 +50,30 @@ All settings can also be configured via the Config Editor UI.
50
50
  | `POST` | `/server-config` | Save DB settings to `.env` |
51
51
  | `POST` | `/test-connection` | Test DB connection with given credentials |
52
52
 
53
+ ## Query Pagination
54
+
55
+ Large result sets are split into pages to avoid sending huge HTTP responses
56
+ (which can fail or time out on slower connections).
57
+
58
+ The query runs **once** — results are cached in memory on the server and
59
+ sliced into pages as the client requests them, so a single `GROUP BY` is
60
+ never re-executed for each page.
61
+
62
+ ```python
63
+ _PAGE_SIZE = 200_000 # rows per page sent to the client
64
+ _QUERY_CACHE_TTL = 300 # seconds a query's results stay in server memory
65
+ ```
66
+
67
+ - `_PAGE_SIZE` — increase if your network/hardware comfortably handles
68
+ larger responses; decrease if you see slow or failing requests on big
69
+ datasets.
70
+ - `_QUERY_CACHE_TTL` — how long the server keeps a query's full result in
71
+ memory while the client fetches subsequent pages. Stale entries are
72
+ cleaned up automatically.
73
+
74
+ The client (`RestProvider`) handles pagination transparently — it requests
75
+ pages in a loop and concatenates them into a single array.
76
+
53
77
  ## Configs
54
78
 
55
79
  Configs are stored as JSON files in `server/configs/`:
@@ -106,7 +106,7 @@
106
106
  ],
107
107
  "measure": "revenue",
108
108
  "func": "sum",
109
- "maxCachedRows": 500000,
109
+ "maxCachedRows": 1000000,
110
110
  "filterCheckboxLimit": 30,
111
111
  "drillthroughQuery": "\n SELECT ID,REGION FROM sales_data\n WHERE {filters}\n LIMIT 200 OFFSET 0\n "
112
112
  }
package/server/server.py CHANGED
@@ -20,6 +20,8 @@ Start:
20
20
  python server.py
21
21
  """
22
22
 
23
+ import time
24
+ import hashlib
23
25
  import json
24
26
  import gzip
25
27
  import os
@@ -38,6 +40,18 @@ CONNECTORS_DIR = os.path.join(BASE_DIR, 'connectors')
38
40
 
39
41
  os.makedirs(CONFIGS_DIR, exist_ok=True)
40
42
 
43
+ _QUERY_CACHE = {} # query hash → {'rows': [...], 'ts': last access time}
44
+ _QUERY_CACHE_TTL = 300 # seconds — how long a cached query result stays in memory
45
+ _PAGE_SIZE = 200_000 # rows per page sent to the client in one HTTP response
46
+
47
+ def _cache_key(query):
48
+ return hashlib.sha256(query.encode('utf-8')).hexdigest()
49
+
50
+ def _cleanup_cache():
51
+ now = time.time()
52
+ for k in [k for k, v in _QUERY_CACHE.items() if now - v['ts'] > _QUERY_CACHE_TTL]:
53
+ del _QUERY_CACHE[k]
54
+
41
55
  # ── Load .env ──────────────────────────────────────────────────────────────────
42
56
 
43
57
  def load_env(path):
@@ -237,10 +251,27 @@ class Handler(BaseHTTPRequestHandler):
237
251
  self._send(400, json.dumps({'error': 'Only SELECT allowed'}))
238
252
  return
239
253
 
254
+ page = int(payload.get('page', 0))
255
+ key = _cache_key(query)
256
+
240
257
  try:
241
- connector = get_active_connector()
242
- rows = connector.execute_query(query)
243
- self._send(200, json.dumps(rows, ensure_ascii=False, default=str))
258
+ if page == 0 or key not in _QUERY_CACHE:
259
+ _cleanup_cache()
260
+ connector = get_active_connector()
261
+ rows = connector.execute_query(query)
262
+ _QUERY_CACHE[key] = {'rows': rows, 'ts': time.time()}
263
+ else:
264
+ _QUERY_CACHE[key]['ts'] = time.time()
265
+
266
+ rows = _QUERY_CACHE[key]['rows']
267
+ total = len(rows)
268
+ start = page * _PAGE_SIZE
269
+ chunk = rows[start:start + _PAGE_SIZE]
270
+ has_more = start + _PAGE_SIZE < total
271
+
272
+ self._send(200, json.dumps({
273
+ 'rows': chunk, 'total': total, 'page': page, 'hasMore': has_more,
274
+ }, ensure_ascii=False, default=str))
244
275
  except Exception as e:
245
276
  self._send(500, json.dumps({'error': str(e)}))
246
277