@protobase/query 0.28.1 → 0.28.2
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/dist/group-totals.d.ts +1 -0
- package/dist/measure.d.ts +2 -2
- package/dist/measure.js +6 -1
- package/dist/series-buckets.d.ts +3 -0
- package/dist/series-buckets.js +12 -4
- package/dist/series.d.ts +2 -1
- package/dist/series.js +4 -4
- package/dist/split-series.d.ts +1 -0
- package/dist/split-series.js +2 -2
- package/package.json +2 -2
package/dist/group-totals.d.ts
CHANGED
package/dist/measure.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ResourceModel } from "@protobase/schema";
|
|
2
2
|
//#region src/measure.d.ts
|
|
3
|
-
/** What a group adds up besides its count: the sum or the
|
|
3
|
+
/** What a group adds up besides its count: the sum, the average or the maximum of a numeric field over its rows. */
|
|
4
4
|
export type Measure = {
|
|
5
|
-
kind: 'sum' | 'average';
|
|
5
|
+
kind: 'sum' | 'average' | 'maximum';
|
|
6
6
|
field: string;
|
|
7
7
|
};
|
|
8
8
|
//#endregion
|
package/dist/measure.js
CHANGED
|
@@ -7,13 +7,18 @@ const numericTypes = [
|
|
|
7
7
|
"bigint",
|
|
8
8
|
"decimal"
|
|
9
9
|
];
|
|
10
|
+
const aggregates = {
|
|
11
|
+
sum: "sum",
|
|
12
|
+
average: "avg",
|
|
13
|
+
maximum: "max"
|
|
14
|
+
};
|
|
10
15
|
/** The SQL aggregate of a measure; select it `::text`, so a decimal keeps its digits until it is read. */
|
|
11
16
|
const measureSql = (model, measure) => {
|
|
12
17
|
const field = findField(model, measure.field);
|
|
13
18
|
if (!numericTypes.includes(field.type)) throw new QueryError("unsupported_field_type", `A ${measure.kind} needs a numeric field, "${measure.field}" is ${field.type}`);
|
|
14
19
|
if (field.sensitive) throw new QueryError("unsupported_field_type", `"${measure.field}" is sensitive, so it cannot be added up`);
|
|
15
20
|
const column = columnRef(field);
|
|
16
|
-
return measure.kind
|
|
21
|
+
return sql`${sql.raw(aggregates[measure.kind])}(${column})`;
|
|
17
22
|
};
|
|
18
23
|
/** A measure's value: `null` when no row of the group has a value to add up. */
|
|
19
24
|
const measureValue = (raw) => raw === null || raw === void 0 ? null : Number(raw);
|
package/dist/series-buckets.d.ts
CHANGED
package/dist/series-buckets.js
CHANGED
|
@@ -5,12 +5,19 @@ import { sql } from "kysely";
|
|
|
5
5
|
//#region src/series-buckets.ts
|
|
6
6
|
const maxBuckets = 1e4;
|
|
7
7
|
const approxMs = {
|
|
8
|
+
minute: 6e4,
|
|
9
|
+
"5_minutes": 3e5,
|
|
10
|
+
"15_minutes": 9e5,
|
|
8
11
|
hour: 36e5,
|
|
9
12
|
day: 864e5,
|
|
10
13
|
week: 6048e5,
|
|
11
14
|
month: 26298e5,
|
|
12
15
|
year: 315576e5
|
|
13
16
|
};
|
|
17
|
+
const minuteStrides = {
|
|
18
|
+
"5_minutes": "5 minutes",
|
|
19
|
+
"15_minutes": "15 minutes"
|
|
20
|
+
};
|
|
14
21
|
const checkTimeZone = (timeZone) => {
|
|
15
22
|
try {
|
|
16
23
|
new Intl.DateTimeFormat("en", { timeZone });
|
|
@@ -35,8 +42,9 @@ const seriesBuckets = (model, field, options) => {
|
|
|
35
42
|
if (!(spanMs > 0)) throw new QueryError("invalid_option", "range.to must be after range.from");
|
|
36
43
|
if (spanMs / approxMs[options.granularity] > maxBuckets) throw new QueryError("invalid_option", `Range spans more than ${maxBuckets} buckets`);
|
|
37
44
|
const col = columnRef(target);
|
|
38
|
-
const
|
|
39
|
-
const step = sql`${sql.lit(`1 ${options.granularity}`)}::interval`;
|
|
45
|
+
const stride = minuteStrides[options.granularity];
|
|
46
|
+
const step = sql`${sql.lit(stride ?? `1 ${options.granularity}`)}::interval`;
|
|
47
|
+
const truncate = (value) => stride ? sql`date_bin(${step}, ${value}, timestamp '2000-01-01')` : sql`date_trunc(${sql.lit(options.granularity)}, ${value})`;
|
|
40
48
|
const isDate = target.type === "date";
|
|
41
49
|
const bound = sql.raw(isDate ? "date" : "timestamptz");
|
|
42
50
|
const local = (value) => isDate ? sql`${value}::timestamp` : sql`(${value}::timestamptz at time zone ${timeZone}::text)`;
|
|
@@ -45,11 +53,11 @@ const seriesBuckets = (model, field, options) => {
|
|
|
45
53
|
col,
|
|
46
54
|
from,
|
|
47
55
|
to,
|
|
48
|
-
|
|
56
|
+
truncate,
|
|
49
57
|
step,
|
|
50
58
|
bound,
|
|
51
59
|
local,
|
|
52
|
-
laterBucket: sql
|
|
60
|
+
laterBucket: truncate(sql`${local(to)} - interval '1 microsecond'`),
|
|
53
61
|
isDate
|
|
54
62
|
};
|
|
55
63
|
};
|
package/dist/series.d.ts
CHANGED
|
@@ -15,9 +15,10 @@ export type SeriesPoint = {
|
|
|
15
15
|
count: number;
|
|
16
16
|
sum?: number | null;
|
|
17
17
|
average?: number | null;
|
|
18
|
+
maximum?: number | null;
|
|
18
19
|
};
|
|
19
20
|
/**
|
|
20
|
-
* Row counts per calendar bucket, in `timeZone`, with empty buckets present. `to` is exclusive.
|
|
21
|
+
* Row counts per calendar bucket (or of 1, 5 or 15 minutes), in `timeZone`, with empty buckets present. `to` is exclusive.
|
|
21
22
|
* Buckets are returned as local ISO strings without offset. `date` fields ignore the time zone;
|
|
22
23
|
* `timestamp` columns without a zone are read in the session time zone. With a measure, a bucket without a row to add
|
|
23
24
|
* up has `null` for it.
|
package/dist/series.js
CHANGED
|
@@ -6,24 +6,24 @@ import { seriesBuckets } from "./series-buckets.js";
|
|
|
6
6
|
import { sql } from "kysely";
|
|
7
7
|
//#region src/series.ts
|
|
8
8
|
/**
|
|
9
|
-
* Row counts per calendar bucket, in `timeZone`, with empty buckets present. `to` is exclusive.
|
|
9
|
+
* Row counts per calendar bucket (or of 1, 5 or 15 minutes), in `timeZone`, with empty buckets present. `to` is exclusive.
|
|
10
10
|
* Buckets are returned as local ISO strings without offset. `date` fields ignore the time zone;
|
|
11
11
|
* `timestamp` columns without a zone are read in the session time zone. With a measure, a bucket without a row to add
|
|
12
12
|
* up has `null` for it.
|
|
13
13
|
*/
|
|
14
14
|
const series = async (db, model, field, options) => {
|
|
15
|
-
const { col, from, to,
|
|
15
|
+
const { col, from, to, truncate, step, bound, local, laterBucket, isDate } = seriesBuckets(model, field, options);
|
|
16
16
|
const { measure } = options;
|
|
17
17
|
const amount = measure ? measureSql(model, measure) : void 0;
|
|
18
18
|
await assertFilterRequirements(db, options.filter, options.scope);
|
|
19
19
|
const counts = baseQuery(db, model, options.filter, options.scope).where(sql`${col} >= ${from}::${bound} and ${col} < ${to}::${bound}`).select([
|
|
20
|
-
|
|
20
|
+
truncate(isDate ? sql`${col}::timestamp` : local(col)).as("bucket"),
|
|
21
21
|
sql`count(*)::text`.as("n"),
|
|
22
22
|
...amount ? [sql`${amount}::text`.as("m")] : []
|
|
23
23
|
]).groupBy(sql`1`);
|
|
24
24
|
return (await sql`
|
|
25
25
|
with b as (
|
|
26
|
-
select generate_series(
|
|
26
|
+
select generate_series(${truncate(local(from))}, ${laterBucket}, ${step}) as bucket
|
|
27
27
|
), c as (${counts})
|
|
28
28
|
select to_char(b.bucket, 'YYYY-MM-DD"T"HH24:MI:SS') as bucket, coalesce(c.n, '0') as n${amount ? sql`, c.m` : sql``}
|
|
29
29
|
from b left join c on c.bucket = b.bucket
|
package/dist/split-series.d.ts
CHANGED
package/dist/split-series.js
CHANGED
|
@@ -21,7 +21,7 @@ const unsplittable = [
|
|
|
21
21
|
* where a value has rows. What the other values hold is the bucket's total from `series` minus its points.
|
|
22
22
|
*/
|
|
23
23
|
const splitSeries = async (db, model, field, split, options) => {
|
|
24
|
-
const { col, from, to,
|
|
24
|
+
const { col, from, to, truncate, bound, local, isDate } = seriesBuckets(model, field, options);
|
|
25
25
|
const splitField = requireField(model, split, "filterable");
|
|
26
26
|
if (unsplittable.includes(splitField.type)) throw new QueryError("unsupported_field_type", `Cannot split by "${split}", which is ${splitField.type}`);
|
|
27
27
|
if (splitField.sensitive) throw new QueryError("unsupported_field_type", `"${split}" is sensitive, so it cannot be split by`);
|
|
@@ -32,7 +32,7 @@ const splitSeries = async (db, model, field, split, options) => {
|
|
|
32
32
|
const amount = measure ? measureSql(model, measure) : void 0;
|
|
33
33
|
await assertFilterRequirements(db, options.filter, options.scope);
|
|
34
34
|
const splitCol = columnRef(splitField);
|
|
35
|
-
const bucketSql = sql`to_char(
|
|
35
|
+
const bucketSql = sql`to_char(${truncate(isDate ? sql`${col}::timestamp` : local(col))}, 'YYYY-MM-DD"T"HH24:MI:SS')`;
|
|
36
36
|
const parts = baseQuery(db, model, options.filter, options.scope).where(sql`${col} >= ${from}::${bound} and ${col} < ${to}::${bound}`).select([
|
|
37
37
|
bucketSql.as("bucket"),
|
|
38
38
|
sql`${splitCol}`.as("split"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/query",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"description": "Kysely list, filter, count and aggregate queries over Protobase resources.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@protobase/schema": "0.28.
|
|
32
|
+
"@protobase/schema": "0.28.2",
|
|
33
33
|
"kysely": "^0.29.6"
|
|
34
34
|
}
|
|
35
35
|
}
|