@saltcorn/qlik-qvd 0.1.0 → 0.1.1

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node -c index.js)"
5
+ ]
6
+ }
7
+ }
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # qlik-qvd
2
+
3
+ Provides the import_qvd_file function, which takes as argumnets:
4
+
5
+ - a filename (a file in the file store)
6
+ - optionally a table name (otherwise it will use the name found in the QVD file)
7
+
8
+ and when run:
9
+
10
+ 1. If the table does not exist, creates it with the fields in the QVD file
11
+ 2. Imports the rows in the QVD file into the table
package/index.js CHANGED
@@ -2,17 +2,49 @@ const File = require("@saltcorn/data/models/file");
2
2
  const Table = require("@saltcorn/data/models/table");
3
3
  const Field = require("@saltcorn/data/models/field");
4
4
  const { getState } = require("@saltcorn/data/db/state");
5
+ const db = require("@saltcorn/data/db");
6
+
7
+ const { Readable } = require("stream");
5
8
 
6
9
  const { QvdDataFrame, QvdFileReader } = require("qvd4js");
7
10
 
11
+ // Serialise a single value into a CSV cell suitable for a Postgres
12
+ // `COPY ... FROM STDIN CSV HEADER` ingest. An empty cell becomes NULL.
13
+ const csvCell = (v) => {
14
+ if (v === null || v === undefined) return "";
15
+ let s;
16
+ if (v instanceof Date) s = v.toISOString();
17
+ else s = String(v);
18
+ if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
19
+ return s;
20
+ };
21
+
8
22
  const numberFormatToType = (nf) => {
9
- if (nf.Type === "REAL") return "Float";
10
- if (nf.Type === "INTEGER") return "Integer";
11
- if (nf.Type === "TIME") return "String";
12
- if (nf.Type === "UNKNOWN") return "String";
23
+ if (nf.Type === "REAL") return { type: "Float" };
24
+ if (nf.Type === "INTEGER") return { type: "Integer" };
25
+ if (nf.Type === "TIME") return { type: "String" };
26
+ if (nf.Type === "UNKNOWN") return { type: "String" };
27
+ if (nf.Type === "DATE")
28
+ return {
29
+ type: "Date",
30
+ attributes: nf.Fmt === "DD.MM.YYYY" ? { day_only: true } : {},
31
+ };
32
+ if (nf.Type === "TIMESTAMP")
33
+ return {
34
+ type: "Date",
35
+ };
13
36
  throw new Error("Unknown NumberFormat: " + JSON.stringify(nf));
14
37
  };
15
38
 
39
+ const QLIK_EPOCH_MS = Date.UTC(1899, 11, 30); // 1899-12-30 00:00:00 (month is 0-indexed)
40
+ const MS_PER_DAY = 86400000;
41
+
42
+ function fromQlik(serial, { roundToSeconds = true } = {}) {
43
+ let ms = QLIK_EPOCH_MS + serial * MS_PER_DAY;
44
+ if (roundToSeconds) ms = Math.round(ms / 1000) * 1000;
45
+ return new Date(ms);
46
+ }
47
+
16
48
  module.exports = {
17
49
  sc_plugin_api_version: 1,
18
50
  plugin_name: "qlik-qvd",
@@ -36,23 +68,65 @@ module.exports = {
36
68
  const fld = {
37
69
  table,
38
70
  label: field.FieldName,
39
- type: numberFormatToType(field.NumberFormat),
40
- attributes: {},
71
+ ...numberFormatToType(field.NumberFormat),
41
72
  };
42
73
 
43
74
  const f = await Field.create(fld);
44
75
  field_names.push(f.name);
45
76
  }
46
- await getState.refresh_tables()
77
+ await getState().refresh_tables();
47
78
  } else {
48
79
  field_names = fields.map((f) => Field.labelToName(f.FieldName));
49
80
  }
50
- for (const row of df.data) {
51
- const o = {};
52
- for (let index = 0; index < field_names.length; index++) {
53
- o[field_names[index]] = row[index];
81
+ const timeStampFields = new Set(
82
+ table.fields
83
+ .filter((f) => f.type?.name === "Date" && !f.attributes?.day_only)
84
+ .map((f) => f.name),
85
+ );
86
+
87
+ // Convert the raw QVD value for a given target field to the value we
88
+ // want stored, applying the Qlik-serial -> Date conversion for
89
+ // timestamp fields (mirrors the row-by-row path below).
90
+ const convertValue = (fname, val) => {
91
+ if (timeStampFields.has(fname) && typeof val === "number")
92
+ return fromQlik(val);
93
+ return val;
94
+ };
95
+
96
+ // Fast path: bulk-load via Postgres COPY when the driver supports it.
97
+ // db.copyFrom is only defined on the Postgres driver, not SQLite.
98
+ if (db.copyFrom) {
99
+ // Stream a CSV of the QVD data, using the plugin's field names as the
100
+ // header, instead of issuing one insertRow per row.
101
+ const csvStream = Readable.from(
102
+ (function* () {
103
+ yield field_names.map(csvCell).join(",") + "\n";
104
+ for (const row of df.data) {
105
+ const cells = new Array(field_names.length);
106
+ for (let index = 0; index < field_names.length; index++) {
107
+ const fname = field_names[index];
108
+ cells[index] = csvCell(convertValue(fname, row[index]));
109
+ }
110
+ yield cells.join(",") + "\n";
111
+ }
112
+ })(),
113
+ );
114
+
115
+ const client = await db.getClient();
116
+ try {
117
+ await db.copyFrom(csvStream, table.name, field_names, client);
118
+ } finally {
119
+ await client.release(true);
120
+ }
121
+ } else {
122
+ for (const row of df.data) {
123
+ const o = {};
124
+ for (let index = 0; index < field_names.length; index++) {
125
+ const fname = field_names[index];
126
+ o[fname] = convertValue(fname, row[index]);
127
+ }
128
+ await table.insertRow(o);
54
129
  }
55
- await table.insertRow(o);
56
130
  }
57
131
  },
58
132
  description: "Convert a list of JSON objects to a CSV string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/qlik-qvd",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Interacting with Qlik QVD files",
5
5
  "main": "index.js",
6
6
  "dependencies": {