@protobase/query 0.13.0 → 0.13.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.
@@ -0,0 +1,7 @@
1
+ import { Db } from "./db.js";
2
+ import { Scope } from "./scope.js";
3
+ import { CheckedFilter, ResourceModel } from "@protobase/schema";
4
+ //#region src/count-up-to.d.ts
5
+ /** How many rows match, counting no further than `limit`: enough to tell a few rows from many without a full count. */
6
+ export declare const countUpTo: (db: Db, model: ResourceModel, limit: number, filter?: CheckedFilter, scope?: Scope) => Promise<number>;
7
+ //#endregion
@@ -0,0 +1,14 @@
1
+ import { baseQuery } from "./base-query.js";
2
+ import { assertFilterRequirements } from "./filter-requirements.js";
3
+ import { toNumber } from "./numbers.js";
4
+ import { sql } from "kysely";
5
+ //#region src/count-up-to.ts
6
+ /** How many rows match, counting no further than `limit`: enough to tell a few rows from many without a full count. */
7
+ const countUpTo = async (db, model, limit, filter, scope) => {
8
+ await assertFilterRequirements(db, filter, scope);
9
+ const found = baseQuery(db, model, filter, scope).select(sql`1`.as("one")).limit(limit).as("found");
10
+ const row = await db.selectFrom(found).select(sql`count(*)::text`.as("n")).executeTakeFirstOrThrow();
11
+ return toNumber(row.n);
12
+ };
13
+ //#endregion
14
+ export { countUpTo };
package/dist/index.d.ts CHANGED
@@ -4,11 +4,13 @@ import { Scope } from "./scope.js";
4
4
  import { ExtraColumn, ListOptions, ListPlan, ListResult, buildListQuery, listQuery, maxLimit } from "./list-query.js";
5
5
  import { estimateCount } from "./estimate-count.js";
6
6
  import { exactCount } from "./exact-count.js";
7
+ import { countUpTo } from "./count-up-to.js";
7
8
  import { FacetCount, FacetOptions, facetCounts } from "./facet-counts.js";
8
9
  import { Granularity, SeriesOptions, SeriesPoint, series } from "./series.js";
9
10
  import { Histogram, HistogramBucket, histogram } from "./histogram.js";
11
+ import { MapBounds, MapBucket, MapBuckets, MapPosition, cellsAcross, checkMapPosition, mapBuckets, mapCellPixels, maxMapBuckets, maxMapZoom } from "./map-buckets.js";
10
12
  import { ScrollMode, scrollMode } from "./scroll-mode.js";
11
13
  import { GuardFinding, GuardOptions, GuardReport, explainGuard } from "./explain-guard.js";
12
14
  import { QueryError, QueryErrorCode } from "./fields/query-error.js";
13
15
  import { SeekRequest, seekToPosition } from "./seek-to-position.js";
14
- export { type CompileOptions, type Db, type ExtraColumn, type FacetCount, type FacetOptions, type Granularity, type GuardFinding, type GuardOptions, type GuardReport, type Histogram, type HistogramBucket, type ListOptions, type ListPlan, type ListResult, QueryError, type QueryErrorCode, type Scope, type ScrollMode, type SeekRequest, type SeriesOptions, type SeriesPoint, buildListQuery, compileFilter, estimateCount, exactCount, explainGuard, facetCounts, histogram, listQuery, maxLimit, scrollMode, seekToPosition, series };
16
+ export { type CompileOptions, type Db, type ExtraColumn, type FacetCount, type FacetOptions, type Granularity, type GuardFinding, type GuardOptions, type GuardReport, type Histogram, type HistogramBucket, type ListOptions, type ListPlan, type ListResult, type MapBounds, type MapBucket, type MapBuckets, type MapPosition, QueryError, type QueryErrorCode, type Scope, type ScrollMode, type SeekRequest, type SeriesOptions, type SeriesPoint, buildListQuery, cellsAcross, checkMapPosition, compileFilter, countUpTo, estimateCount, exactCount, explainGuard, facetCounts, histogram, listQuery, mapBuckets, mapCellPixels, maxLimit, maxMapBuckets, maxMapZoom, scrollMode, seekToPosition, series };
package/dist/index.js CHANGED
@@ -3,10 +3,12 @@ import { compileFilter } from "./compile-filter.js";
3
3
  import { buildListQuery, listQuery, maxLimit } from "./list-query.js";
4
4
  import { estimateCount } from "./estimate-count.js";
5
5
  import { exactCount } from "./exact-count.js";
6
+ import { countUpTo } from "./count-up-to.js";
6
7
  import { facetCounts } from "./facet-counts.js";
7
8
  import { series } from "./series.js";
8
9
  import { histogram } from "./histogram.js";
10
+ import { cellsAcross, checkMapPosition, mapBuckets, mapCellPixels, maxMapBuckets, maxMapZoom } from "./map-buckets.js";
9
11
  import { scrollMode } from "./scroll-mode.js";
10
12
  import { explainGuard } from "./explain-guard.js";
11
13
  import { seekToPosition } from "./seek-to-position.js";
12
- export { QueryError, buildListQuery, compileFilter, estimateCount, exactCount, explainGuard, facetCounts, histogram, listQuery, maxLimit, scrollMode, seekToPosition, series };
14
+ export { QueryError, buildListQuery, cellsAcross, checkMapPosition, compileFilter, countUpTo, estimateCount, exactCount, explainGuard, facetCounts, histogram, listQuery, mapBuckets, mapCellPixels, maxLimit, maxMapBuckets, maxMapZoom, scrollMode, seekToPosition, series };
@@ -0,0 +1,48 @@
1
+ import { Db } from "./db.js";
2
+ import { Scope } from "./scope.js";
3
+ import { CheckedFilter, ResourceModel } from "@protobase/schema";
4
+ //#region src/map-buckets.d.ts
5
+ /** MapLibre's zoom levels, from the whole world at 0 to a building at 22. */
6
+ export declare const maxMapZoom = 22;
7
+ /** About how many pixels across a bucket's cell is on the map. */
8
+ export declare const mapCellPixels = 64;
9
+ /** The most buckets one request returns, the fullest first; a large screen's map holds fewer cells. */
10
+ export declare const maxMapBuckets = 1000;
11
+ /** The fields that hold a record's position, in decimal degrees. */
12
+ export type MapPosition = {
13
+ latitude: string;
14
+ longitude: string;
15
+ };
16
+ /** The corners of the records a bucket stands for, in decimal degrees. */
17
+ export type MapBounds = {
18
+ south: number;
19
+ west: number;
20
+ north: number;
21
+ east: number;
22
+ };
23
+ /** The records in one cell of the map's grid: how many, their mean position, and the bounds around them. */
24
+ export type MapBucket = {
25
+ count: number;
26
+ latitude: number;
27
+ longitude: number;
28
+ bounds: MapBounds;
29
+ };
30
+ /** The buckets, and how many records they hold together, the ones beyond `maxMapBuckets` included. */
31
+ export type MapBuckets = {
32
+ total: number;
33
+ buckets: MapBucket[];
34
+ };
35
+ /** How many cells span the world's width, and its Web Mercator height, at a zoom. */
36
+ export declare const cellsAcross: (zoom: number) => number;
37
+ /** The position's fields, refused unless each is a filterable number of degrees. */
38
+ export declare const checkMapPosition: (model: ResourceModel, position: MapPosition) => {
39
+ latitude: import("@protobase/schema").FieldModel;
40
+ longitude: import("@protobase/schema").FieldModel;
41
+ };
42
+ /**
43
+ * The matching records with a position, counted per cell of a grid that is square on the map at `zoom` (Web
44
+ * Mercator, as MapLibre draws it) and about `mapCellPixels` across. The grid starts at the antimeridian and the north
45
+ * edge, so a cell stays put as the map pans. Postgres groups the rows, so the filter can use the position's indexes.
46
+ */
47
+ export declare const mapBuckets: (db: Db, model: ResourceModel, position: MapPosition, zoom: number, filter?: CheckedFilter, scope?: Scope) => Promise<MapBuckets>;
48
+ //#endregion
@@ -0,0 +1,75 @@
1
+ import { QueryError } from "./fields/query-error.js";
2
+ import { columnRef, requireField } from "./columns.js";
3
+ import { baseQuery } from "./base-query.js";
4
+ import { assertFilterRequirements } from "./filter-requirements.js";
5
+ import { toNumber } from "./numbers.js";
6
+ import { sql } from "kysely";
7
+ //#region src/map-buckets.ts
8
+ /** MapLibre's zoom levels, from the whole world at 0 to a building at 22. */
9
+ const maxMapZoom = 22;
10
+ /** About how many pixels across a bucket's cell is on the map. */
11
+ const mapCellPixels = 64;
12
+ /** The most buckets one request returns, the fullest first; a large screen's map holds fewer cells. */
13
+ const maxMapBuckets = 1e3;
14
+ const worldPixels = 512;
15
+ const maxLatitude = 85.051129;
16
+ const coordinateTypes = [
17
+ "integer",
18
+ "bigint",
19
+ "decimal"
20
+ ];
21
+ /** How many cells span the world's width, and its Web Mercator height, at a zoom. */
22
+ const cellsAcross = (zoom) => worldPixels * 2 ** zoom / 64;
23
+ const coordinate = (model, name) => {
24
+ const target = requireField(model, name, "filterable");
25
+ if (!coordinateTypes.includes(target.type)) throw new QueryError("unsupported_field_type", `A position needs number fields, "${name}" is ${target.type}`);
26
+ return target;
27
+ };
28
+ /** The position's fields, refused unless each is a filterable number of degrees. */
29
+ const checkMapPosition = (model, position) => ({
30
+ latitude: coordinate(model, position.latitude),
31
+ longitude: coordinate(model, position.longitude)
32
+ });
33
+ /**
34
+ * The matching records with a position, counted per cell of a grid that is square on the map at `zoom` (Web
35
+ * Mercator, as MapLibre draws it) and about `mapCellPixels` across. The grid starts at the antimeridian and the north
36
+ * edge, so a cell stays put as the map pans. Postgres groups the rows, so the filter can use the position's indexes.
37
+ */
38
+ const mapBuckets = async (db, model, position, zoom, filter, scope) => {
39
+ const fields = checkMapPosition(model, position);
40
+ if (!Number.isInteger(zoom) || zoom < 0 || zoom > 22) throw new QueryError("invalid_option", `zoom must be a whole number from 0 to 22`);
41
+ await assertFilterRequirements(db, filter, scope);
42
+ const latitude = sql`${columnRef(fields.latitude)}::float8`;
43
+ const longitude = sql`${columnRef(fields.longitude)}::float8`;
44
+ const cells = cellsAcross(zoom);
45
+ const last = cells - 1;
46
+ const clamped = sql`least(greatest(${latitude}, ${-85.051129}::float8), ${maxLatitude}::float8)`;
47
+ const column = sql`least(floor((${longitude} + 180) / 360 * ${cells}::float8), ${last}::float8)`;
48
+ const row = sql`least(floor((1 - ln(tan(pi() / 4 + radians(${clamped}) / 2)) / pi()) / 2 * ${cells}::float8), ${last}::float8)`;
49
+ const rows = await baseQuery(db, model, filter, scope).where(sql`${columnRef(fields.latitude)} is not null and ${columnRef(fields.longitude)} is not null`).select([
50
+ sql`count(*)::text`.as("n"),
51
+ sql`(sum(count(*)) over ())::text`.as("total"),
52
+ sql`avg(${latitude})::text`.as("latitude"),
53
+ sql`avg(${longitude})::text`.as("longitude"),
54
+ sql`min(${latitude})::text`.as("south"),
55
+ sql`min(${longitude})::text`.as("west"),
56
+ sql`max(${latitude})::text`.as("north"),
57
+ sql`max(${longitude})::text`.as("east")
58
+ ]).groupBy([column, row]).orderBy(sql`count(*)`, "desc").limit(maxMapBuckets).execute();
59
+ return {
60
+ total: rows.length === 0 ? 0 : toNumber(rows[0].total),
61
+ buckets: rows.map((bucket) => ({
62
+ count: toNumber(bucket.n),
63
+ latitude: toNumber(bucket.latitude),
64
+ longitude: toNumber(bucket.longitude),
65
+ bounds: {
66
+ south: toNumber(bucket.south),
67
+ west: toNumber(bucket.west),
68
+ north: toNumber(bucket.north),
69
+ east: toNumber(bucket.east)
70
+ }
71
+ }))
72
+ };
73
+ };
74
+ //#endregion
75
+ export { cellsAcross, checkMapPosition, mapBuckets, mapCellPixels, maxMapBuckets, maxMapZoom };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/query",
3
- "version": "0.13.0",
3
+ "version": "0.13.3",
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.13.0",
32
+ "@protobase/schema": "0.13.3",
33
33
  "kysely": "^0.29.6"
34
34
  }
35
35
  }