@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.
@@ -16,6 +16,7 @@ export type GroupTotal = {
16
16
  count: number;
17
17
  sum?: number | null;
18
18
  average?: number | null;
19
+ maximum?: number | null;
19
20
  };
20
21
  /**
21
22
  * Rows per value of `field` under the whole filter, largest first: by the measure when there is one, else by count.
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 average of a numeric field over its rows. */
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 === "sum" ? sql`sum(${column})` : sql`avg(${column})`;
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);
@@ -2,6 +2,9 @@ import { sql } from "kysely";
2
2
  import { ResourceModel } from "@protobase/schema";
3
3
  //#region src/series-buckets.d.ts
4
4
  declare const approxMs: {
5
+ minute: number;
6
+ '5_minutes': number;
7
+ '15_minutes': number;
5
8
  hour: number;
6
9
  day: number;
7
10
  week: number;
@@ -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 unit = sql.lit(options.granularity);
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
- unit,
56
+ truncate,
49
57
  step,
50
58
  bound,
51
59
  local,
52
- laterBucket: sql`date_trunc(${unit}, ${local(to)} - interval '1 microsecond')`,
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, unit, step, bound, local, laterBucket, isDate } = seriesBuckets(model, field, options);
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
- sql`date_trunc(${unit}, ${isDate ? sql`${col}::timestamp` : local(col)})`.as("bucket"),
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(date_trunc(${unit}, ${local(from)}), ${laterBucket}, ${step}) as bucket
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
@@ -12,6 +12,7 @@ export type SplitPoint = {
12
12
  count: number;
13
13
  sum?: number | null;
14
14
  average?: number | null;
15
+ maximum?: number | null;
15
16
  };
16
17
  /**
17
18
  * The rows of each calendar bucket of `field` (as `series` buckets it) split by the values of another field.
@@ -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, unit, bound, local, isDate } = seriesBuckets(model, field, options);
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(date_trunc(${unit}, ${isDate ? sql`${col}::timestamp` : local(col)}), 'YYYY-MM-DD"T"HH24:MI:SS')`;
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.1",
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.1",
32
+ "@protobase/schema": "0.28.2",
33
33
  "kysely": "^0.29.6"
34
34
  }
35
35
  }