flatsql 0.4.0 → 0.4.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/README.md CHANGED
@@ -250,6 +250,106 @@ FlatSQL ingests size-prefixed FlatBuffer streams:
250
250
 
251
251
  The 4-byte file identifier in each FlatBuffer determines which table receives the record.
252
252
 
253
+ ## Spatial Extensions
254
+
255
+ FlatSQL includes a full spatial computation engine with SpatiaLite-compatible functions, polygon boolean operations, Voronoi/Delaunay tessellation, and coordinate transforms.
256
+
257
+ ### Spatial SQL Functions
258
+
259
+ | Category | Functions |
260
+ |----------|-----------|
261
+ | **Point ops** | `geo_distance`, `geo_bearing`, `geo_destination`, `geo_midpoint`, `geo_area_bbox`, `geo_within_radius`, `geo_bbox_contains` |
262
+ | **Geohash** | `geo_geohash_encode`, `geo_geohash_decode_lat`, `geo_geohash_decode_lon` |
263
+ | **Geometry I/O** | `geo_from_text` (WKT→blob), `geo_as_text` (blob→WKT), `geo_from_geojson`, `geo_as_geojson` |
264
+ | **Predicates** | `geo_contains` (point-in-polygon, ray casting with hole support) |
265
+ | **Boolean ops** | `geo_intersection`, `geo_union`, `geo_difference`, `geo_sym_difference`, `geo_buffer` |
266
+ | **Analysis** | `geo_area_geom`, `geo_centroid`, `geo_length_geom`, `geo_envelope`, `geo_convex_hull` |
267
+ | **Voronoi/Delaunay** | `geo_voronoi`, `geo_delaunay` |
268
+ | **Coord transforms** | `geo_to_ecef` (WGS84→ECEF), `geo_from_ecef` (ECEF→WGS84) |
269
+
270
+ ### Examples
271
+
272
+ ```sql
273
+ -- Distance between NYC and DC (~328 km)
274
+ SELECT geo_distance(40.7128, -74.0060, 38.9072, -77.0369);
275
+
276
+ -- Bearing from NYC to LA
277
+ SELECT geo_bearing(40.7128, -74.0060, 34.0522, -118.2437);
278
+
279
+ -- Geohash encode/decode
280
+ SELECT geo_geohash_encode(42.6, -5.6, 5); -- 'ezs42'
281
+
282
+ -- Point-in-polygon
283
+ SELECT geo_contains('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))', 5, 5); -- 1
284
+
285
+ -- Polygon intersection (accepts WKT or blob)
286
+ SELECT geo_as_text(geo_intersection(
287
+ 'POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))',
288
+ 'POLYGON((5 5, 15 5, 15 15, 5 15, 5 5))'
289
+ ));
290
+
291
+ -- WGS84 to ECEF coordinate transform
292
+ SELECT geo_to_ecef(48.8566, 2.3522, 35); -- '4200952.xxx,172458.xxx,4780111.xxx'
293
+
294
+ -- Voronoi diagram
295
+ SELECT geo_as_text(geo_voronoi(
296
+ 'MULTIPOINT((0 0), (10 0), (10 10), (0 10))',
297
+ 'POLYGON((-5 -5, 15 -5, 15 15, -5 15, -5 -5))'
298
+ ));
299
+ ```
300
+
301
+ ### R-Tree Spatial Indexing
302
+
303
+ FlatSQL automatically creates R-Tree shadow tables for spatial columns during ingest. Spatial columns are detected by:
304
+ - **Convention**: columns named `latitude`/`lat` + `longitude`/`lon`/`lng`
305
+ - **Schema attribute**: fields marked with `(spatial)` in FlatBuffers IDL
306
+
307
+ ```sql
308
+ -- R-Tree pre-filter + precise radius check (72x faster than full scan)
309
+ SELECT p.* FROM points p
310
+ INNER JOIN _rtree_points r ON p.rowid = r.id
311
+ WHERE r.minLat >= 36.2 AND r.maxLat <= 45.2
312
+ AND r.minLon >= -80 AND r.maxLon <= -68
313
+ AND geo_within_radius(40.7, -74.0, p.lat, p.lon, 500) = 1;
314
+ ```
315
+
316
+ ### Spatial Performance
317
+
318
+ | Operation | Throughput | Latency |
319
+ |-----------|-----------|---------|
320
+ | Point operations (distance, bearing) | 733K–1.07M ops/sec | ~1 µs |
321
+ | Geohash encode/decode | 631K–746K ops/sec | ~1.4 µs |
322
+ | Point-in-polygon | 127K–255K ops/sec | ~4 µs |
323
+ | Polygon intersection | 81K ops/sec | ~12 µs |
324
+ | Voronoi (4 sites) | 34K ops/sec | ~29 µs |
325
+ | Coordinate transforms | 556K–609K ops/sec | ~1.7 µs |
326
+ | R-Tree bbox query (10K records) | 77K ops/sec | ~13 µs |
327
+ | R-Tree vs full scan speedup | **58–72x** | |
328
+
329
+ ### Space Data Module (SDM)
330
+
331
+ FlatSQL Spatial is also available as a standalone WASM module compliant with the [Space Data Module SDK](https://github.com/DigitalArsenal/space-data-module-sdk):
332
+
333
+ ```javascript
334
+ import { initSpatialSDM } from 'flatsql/sdm';
335
+
336
+ const sdm = await initSpatialSDM();
337
+
338
+ // Read the embedded manifest
339
+ console.log(sdm.getManifest().pluginId);
340
+ // → 'com.digitalarsenal.flatsql.spatial'
341
+
342
+ // Compute distance
343
+ const km = sdm.computeDistance(40.7128, -74.006, 38.9072, -77.0369);
344
+
345
+ // Point-in-polygon
346
+ const inside = sdm.pointInPolygon('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))', 5, 5);
347
+
348
+ // Coordinate transform
349
+ const ecef = sdm.toECEF(48.8566, 2.3522, 35);
350
+ const back = sdm.fromECEF(ecef.x, ecef.y, ecef.z);
351
+ ```
352
+
253
353
  ## SQL Support
254
354
 
255
355
  ### Supported
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flatsql",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "SQL queries over raw FlatBuffer storage - zero conversion, real-time indexing, pure streaming",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,13 +18,22 @@
18
18
  "types": "./wasm/wasi.d.ts",
19
19
  "import": "./wasm/wasi.js"
20
20
  },
21
- "./wasi.wasm": "./wasm/flatsql-wasi.wasm"
21
+ "./wasi.wasm": "./wasm/flatsql-wasi.wasm",
22
+ "./sdm": {
23
+ "types": "./sdm/index.d.ts",
24
+ "import": "./sdm/index.js"
25
+ },
26
+ "./sdm.wasm": "./sdm/flatsql-spatial.wasm"
22
27
  },
23
28
  "files": [
24
29
  "dist",
25
30
  "wasm/*.js",
26
31
  "wasm/*.wasm",
27
32
  "wasm/*.d.ts",
33
+ "sdm/*.js",
34
+ "sdm/*.wasm",
35
+ "sdm/*.d.ts",
36
+ "sdm/*.json",
28
37
  "README.md",
29
38
  "LICENSE"
30
39
  ],
@@ -64,20 +73,26 @@
64
73
  "wasm",
65
74
  "webassembly",
66
75
  "query",
67
- "virtual-table"
76
+ "virtual-table",
77
+ "spatial",
78
+ "geospatial",
79
+ "voronoi",
80
+ "delaunay",
81
+ "rtree",
82
+ "space-data-module"
68
83
  ],
69
84
  "author": "Digital Arsenal <tj@digitalarsenal.io>",
70
85
  "license": "Apache-2.0",
71
86
  "dependencies": {
72
- "flatc-wasm": "^26.1.15",
87
+ "flatc-wasm": "^26.1.32",
73
88
  "sql.js": "^1.10.0"
74
89
  },
75
90
  "devDependencies": {
91
+ "@types/jest": "^29.5.0",
76
92
  "@types/node": "^20.0.0",
77
- "typescript": "^5.3.0",
78
93
  "jest": "^29.7.0",
79
- "@types/jest": "^29.5.0",
80
94
  "ts-jest": "^29.1.0",
95
+ "typescript": "^5.3.0",
81
96
  "vite": "^5.0.0"
82
97
  },
83
98
  "engines": {
Binary file
package/sdm/index.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * FlatSQL Spatial Engine — Space Data Module (SDM)
3
+ * TypeScript type definitions
4
+ *
5
+ * @see https://github.com/DigitalArsenal/space-data-module-sdk
6
+ */
7
+
8
+ export interface ECEFPoint {
9
+ x: number;
10
+ y: number;
11
+ z: number;
12
+ }
13
+
14
+ export interface GeodeticPoint {
15
+ lat: number;
16
+ lon: number;
17
+ alt: number;
18
+ }
19
+
20
+ export interface MethodResult {
21
+ status: number;
22
+ output: string;
23
+ }
24
+
25
+ export interface PluginManifest {
26
+ pluginId: string;
27
+ name: string;
28
+ version: string;
29
+ pluginFamily: string;
30
+ description: string;
31
+ capabilities: string[];
32
+ externalInterfaces: string[];
33
+ methods: Array<{
34
+ methodId: string;
35
+ description: string;
36
+ inputPorts: Array<{
37
+ portId: string;
38
+ schemaNames: string[];
39
+ maxItems: number;
40
+ }>;
41
+ outputPorts: Array<{
42
+ portId: string;
43
+ schemaNames: string[];
44
+ maxItems: number;
45
+ }>;
46
+ maxBatch: number;
47
+ drainPolicy: string;
48
+ }>;
49
+ runtimeTargets: string[];
50
+ }
51
+
52
+ export interface FlatSQLSpatialSDM {
53
+ /** Get the embedded plugin manifest */
54
+ getManifest(): PluginManifest;
55
+
56
+ /** Compute haversine distance between two WGS84 points (km) */
57
+ computeDistance(lat1: number, lon1: number, lat2: number, lon2: number): number;
58
+
59
+ /** Compute forward azimuth between two WGS84 points (degrees 0-360) */
60
+ computeBearing(lat1: number, lon1: number, lat2: number, lon2: number): number;
61
+
62
+ /** Test if a WGS84 point is inside a WKT polygon */
63
+ pointInPolygon(wkt: string, lat: number, lon: number): boolean;
64
+
65
+ /** Compute intersection of two WKT polygons */
66
+ polygonIntersection(wkt1: string, wkt2: string): string;
67
+
68
+ /** Compute union of two WKT polygons */
69
+ polygonUnion(wkt1: string, wkt2: string): string;
70
+
71
+ /** Compute Voronoi diagram from multipoint WKT within bounds */
72
+ computeVoronoi(multipointWkt: string, boundsWkt: string): string;
73
+
74
+ /** Compute Delaunay triangulation from multipoint WKT */
75
+ computeDelaunay(multipointWkt: string): string;
76
+
77
+ /** Transform WGS84 geodetic to ECEF */
78
+ toECEF(lat: number, lon: number, alt: number): ECEFPoint;
79
+
80
+ /** Transform ECEF to WGS84 geodetic */
81
+ fromECEF(x: number, y: number, z: number): GeodeticPoint;
82
+
83
+ /** Low-level method call */
84
+ callMethod(methodName: string, input: string): MethodResult;
85
+ }
86
+
87
+ /** Initialize the FlatSQL Spatial SDM module */
88
+ export function initSpatialSDM(): Promise<FlatSQLSpatialSDM>;
package/sdm/index.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * FlatSQL Spatial Engine — Space Data Module (SDM)
3
+ *
4
+ * Standalone WASM module compliant with space-data-module-sdk.
5
+ * Provides spatial computation functions without requiring full FlatSQL.
6
+ *
7
+ * @see https://github.com/DigitalArsenal/space-data-module-sdk
8
+ */
9
+
10
+ import { readFile } from "node:fs/promises";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join } from "node:path";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ /**
17
+ * Initialize the FlatSQL Spatial SDM module.
18
+ * @returns {Promise<FlatSQLSpatialSDM>}
19
+ */
20
+ export async function initSpatialSDM() {
21
+ const wasmPath = join(__dirname, "flatsql-spatial.wasm");
22
+ const wasmBytes = await readFile(wasmPath);
23
+ let exports;
24
+ const invoke = () => (fnPtr, ...args) => {
25
+ const table = exports?.__indirect_function_table;
26
+ const fn = table?.get(fnPtr);
27
+ if (typeof fn !== "function") {
28
+ throw new Error(`Missing indirect WASM function: ${fnPtr}`);
29
+ }
30
+ return fn(...args);
31
+ };
32
+ const { instance } = await WebAssembly.instantiate(wasmBytes, {
33
+ env: {
34
+ invoke_v: invoke("v"),
35
+ invoke_i: invoke("i"),
36
+ invoke_ii: invoke("ii"),
37
+ invoke_iii: invoke("iii"),
38
+ invoke_iiii: invoke("iiii"),
39
+ invoke_iiiii: invoke("iiiii"),
40
+ invoke_iiiiii: invoke("iiiiii"),
41
+ invoke_iiiiiii: invoke("iiiiiii"),
42
+ invoke_iiiiiiii: invoke("iiiiiiii"),
43
+ invoke_iiiiiiiiiiii: invoke("iiiiiiiiiiii"),
44
+ invoke_iiiiid: invoke("iiiiid"),
45
+ invoke_di: invoke("di"),
46
+ invoke_dii: invoke("dii"),
47
+ invoke_diii: invoke("diii"),
48
+ invoke_fiii: invoke("fiii"),
49
+ invoke_iid: invoke("iid"),
50
+ invoke_jiiii: invoke("jiiii"),
51
+ invoke_vi: invoke("vi"),
52
+ invoke_vid: invoke("vid"),
53
+ invoke_vii: invoke("vii"),
54
+ invoke_viii: invoke("viii"),
55
+ invoke_viiii: invoke("viiii"),
56
+ invoke_viiiii: invoke("viiiii"),
57
+ invoke_viiiiiii: invoke("viiiiiii"),
58
+ invoke_viiiiiiiiii: invoke("viiiiiiiiii"),
59
+ invoke_viiiiiiiiiiiiiii: invoke("viiiiiiiiiiiiiii"),
60
+ __cxa_begin_catch: (ptr) => ptr,
61
+ __cxa_end_catch: () => {},
62
+ __cxa_find_matching_catch_2: () => 0,
63
+ __cxa_find_matching_catch_3: () => 0,
64
+ __cxa_rethrow: () => {},
65
+ __cxa_throw: () => {},
66
+ __cxa_uncaught_exceptions: () => 0,
67
+ __resumeException: (ptr) => {
68
+ throw new Error(`Unhandled WASM exception: ${ptr}`);
69
+ },
70
+ emscripten_notify_memory_growth: () => {},
71
+ },
72
+ wasi_snapshot_preview1: {
73
+ environ_get: () => 0,
74
+ environ_sizes_get: (environCount, environBufSize) => {
75
+ const memory = exports?.memory;
76
+ if (memory) {
77
+ const view = new DataView(memory.buffer);
78
+ view.setUint32(environCount, 0, true);
79
+ view.setUint32(environBufSize, 0, true);
80
+ }
81
+ return 0;
82
+ },
83
+ proc_exit: () => {},
84
+ fd_write: () => 0,
85
+ fd_seek: () => 0,
86
+ fd_close: () => 0,
87
+ },
88
+ });
89
+ exports = instance.exports;
90
+ const memory = /** @type {WebAssembly.Memory} */ (exports.memory);
91
+
92
+ /**
93
+ * Write a string to the input buffer.
94
+ * @param {string} str
95
+ */
96
+ function writeInput(str) {
97
+ const inputPtr = /** @type {Function} */ (exports.sdm_get_input_buffer)();
98
+ const encoder = new TextEncoder();
99
+ const bytes = encoder.encode(str);
100
+ const view = new Uint8Array(memory.buffer, inputPtr, bytes.length);
101
+ view.set(bytes);
102
+ /** @type {Function} */ (exports.sdm_set_input_length)(bytes.length);
103
+ }
104
+
105
+ /**
106
+ * Read the output buffer as a string.
107
+ * @returns {string}
108
+ */
109
+ function readOutput() {
110
+ const outputLen = /** @type {Function} */ (exports.sdm_get_output_length)();
111
+ const outputPtr = /** @type {Function} */ (exports.sdm_get_output_buffer)();
112
+ const view = new Uint8Array(memory.buffer, outputPtr, outputLen);
113
+ return new TextDecoder().decode(view);
114
+ }
115
+
116
+ /**
117
+ * Call a method with string input, return string output.
118
+ * @param {string} methodName
119
+ * @param {string} input
120
+ * @returns {{ status: number, output: string }}
121
+ */
122
+ function callMethod(methodName, input) {
123
+ writeInput(input);
124
+ const fn = exports[methodName];
125
+ if (!fn) throw new Error(`Unknown method: ${methodName}`);
126
+ const status = /** @type {Function} */ (fn)();
127
+ return { status, output: readOutput() };
128
+ }
129
+
130
+ /**
131
+ * Get the embedded plugin manifest.
132
+ * @returns {object}
133
+ */
134
+ function getManifest() {
135
+ const ptr = /** @type {Function} */ (exports.plugin_get_manifest)();
136
+ const size = /** @type {Function} */ (exports.plugin_get_manifest_size)();
137
+ const view = new Uint8Array(memory.buffer, ptr, size);
138
+ const text = new TextDecoder().decode(view);
139
+ return JSON.parse(text);
140
+ }
141
+
142
+ return {
143
+ /** Get the plugin manifest */
144
+ getManifest,
145
+
146
+ /**
147
+ * Compute haversine distance between two points.
148
+ * @param {number} lat1 @param {number} lon1
149
+ * @param {number} lat2 @param {number} lon2
150
+ * @returns {number} distance in km
151
+ */
152
+ computeDistance(lat1, lon1, lat2, lon2) {
153
+ const { status, output } = callMethod("compute_distance", `${lat1},${lon1},${lat2},${lon2}`);
154
+ if (status !== 0) throw new Error(output);
155
+ return parseFloat(output);
156
+ },
157
+
158
+ /**
159
+ * Compute bearing from point 1 to point 2.
160
+ * @param {number} lat1 @param {number} lon1
161
+ * @param {number} lat2 @param {number} lon2
162
+ * @returns {number} bearing in degrees (0-360)
163
+ */
164
+ computeBearing(lat1, lon1, lat2, lon2) {
165
+ const { status, output } = callMethod("compute_bearing", `${lat1},${lon1},${lat2},${lon2}`);
166
+ if (status !== 0) throw new Error(output);
167
+ return parseFloat(output);
168
+ },
169
+
170
+ /**
171
+ * Test if a point is inside a polygon.
172
+ * @param {string} wkt - WKT polygon
173
+ * @param {number} lat @param {number} lon
174
+ * @returns {boolean}
175
+ */
176
+ pointInPolygon(wkt, lat, lon) {
177
+ const { status, output } = callMethod("point_in_polygon", `${wkt}\n${lat},${lon}`);
178
+ if (status !== 0) throw new Error(output);
179
+ return output === "1";
180
+ },
181
+
182
+ /**
183
+ * Compute polygon intersection.
184
+ * @param {string} wkt1 @param {string} wkt2
185
+ * @returns {string} WKT of intersection
186
+ */
187
+ polygonIntersection(wkt1, wkt2) {
188
+ const { status, output } = callMethod("polygon_intersection", `${wkt1}\n${wkt2}`);
189
+ if (status !== 0) throw new Error(output);
190
+ return output;
191
+ },
192
+
193
+ /**
194
+ * Compute polygon union.
195
+ * @param {string} wkt1 @param {string} wkt2
196
+ * @returns {string} WKT of union
197
+ */
198
+ polygonUnion(wkt1, wkt2) {
199
+ const { status, output } = callMethod("polygon_union", `${wkt1}\n${wkt2}`);
200
+ if (status !== 0) throw new Error(output);
201
+ return output;
202
+ },
203
+
204
+ /**
205
+ * Compute Voronoi diagram.
206
+ * @param {string} multipointWkt @param {string} boundsWkt
207
+ * @returns {string} MULTIPOLYGON WKT
208
+ */
209
+ computeVoronoi(multipointWkt, boundsWkt) {
210
+ const { status, output } = callMethod("compute_voronoi", `${multipointWkt}\n${boundsWkt}`);
211
+ if (status !== 0) throw new Error(output);
212
+ return output;
213
+ },
214
+
215
+ /**
216
+ * Compute Delaunay triangulation.
217
+ * @param {string} multipointWkt
218
+ * @returns {string} MULTIPOLYGON WKT
219
+ */
220
+ computeDelaunay(multipointWkt) {
221
+ const { status, output } = callMethod("compute_delaunay", multipointWkt);
222
+ if (status !== 0) throw new Error(output);
223
+ return output;
224
+ },
225
+
226
+ /**
227
+ * Transform WGS84 to ECEF.
228
+ * @param {number} lat @param {number} lon @param {number} alt
229
+ * @returns {{ x: number, y: number, z: number }}
230
+ */
231
+ toECEF(lat, lon, alt) {
232
+ const { status, output } = callMethod("transform_to_ecef", `${lat},${lon},${alt}`);
233
+ if (status !== 0) throw new Error(output);
234
+ const [x, y, z] = output.split(",").map(Number);
235
+ return { x, y, z };
236
+ },
237
+
238
+ /**
239
+ * Transform ECEF to WGS84.
240
+ * @param {number} x @param {number} y @param {number} z
241
+ * @returns {{ lat: number, lon: number, alt: number }}
242
+ */
243
+ fromECEF(x, y, z) {
244
+ const { status, output } = callMethod("transform_from_ecef", `${x},${y},${z}`);
245
+ if (status !== 0) throw new Error(output);
246
+ const [lat, lon, alt] = output.split(",").map(Number);
247
+ return { lat, lon, alt };
248
+ },
249
+
250
+ /** Low-level method call */
251
+ callMethod,
252
+ };
253
+ }