newmo ã§ã¯ãå°å³ãã¼ã¿ãå°çæ å ±ãæ±ãå ´é¢ãå¤ãããã¾ãã
ãã¨ãã°ãã¿ã¯ã·ã¼ãã©ã¤ãã·ã§ã¢ã§ã¯ãå¶æ¥åºåã®ãããªå¶æ¥ãã¦ããã¨ãªã¢ã¨ãã£ãå°ççãªå®ç¾©ãããã¾ãã ã¾ããä¹ãå ¥ãç¦æ¢åºåã®ãããªã¿ã¯ã·ã¼ãä¹ãå ¥ãã¦ã¯ãããªãã¨ãªã¢ã¨ãã£ãå®ç¾©ãå¿ è¦ã«ãªãã¾ãã
ãããã®å°çã«é¢ããå®ç¾©ã¯ GeoJSON ã®ãããªå°çæ å ±ãæ±ããã¼ã¿å½¢å¼ã§ç®¡çããããã¨ãå¤ãã§ãã ããããGeoJSONãªã©ã®å®ç¾©ãããã¹ãã¨ãã¦ææ¸ãããã®ã¯å°é£ã§ãã ãã®ãããå°å³ä¸ã«åºåãä½å³ããã¨ãã£ã¿ããã®å®ç¾©ããåºåãæ£ãããããã§ãã¯ãããããªç®¡çãã¼ã«ãå¿ è¦ã§ãã 管çãã¼ã«ã¯ãã¦ã§ãã¢ããªã±ã¼ã·ã§ã³ã¨ãã¦ä½ã£ãæ¹ãå©ç¨ã§ããç°å¢ãåºããã¾ãã
ãã®ãããªå°çæ å ±ã¯ä¸åº¦ã«æ±ããã¼ã¿ãå¤ãã£ããã空éçãªè¨ç®å¦çãå¿ è¦ã«ãªããããå°ç¨ã®ä»çµã¿ã使ããã¨ãå¤ãã§ãã ãã®ãããªæè¡ããå°çæ å ±ã·ã¹ãã ï¼GISï¼Geographic Information Systemï¼ã¨å¼ã³ã¾ãã
ãã©ã¦ã¶ãã¦ã§ãæ¨æºã«ã¯ãGISå¦çãè¡ãä»çµã¿ã¯ç¹ã«ãªãããããã§ã«å®å®ããGISã®å®è£ ããããµã¼ãå´ã§å¦çãããã¨ãå¤ãã§ãã ãããããã¼ã¿ã夿´ãããã³ã«ãã¯ã©ã¤ã¢ã³ããããµã¼ãã«ãªã¯ã¨ã¹ããã¦ãµã¼ãå´ã§ GIS ã®å¦çãè¡ãã¨ã夿´ã®åæ ããªã¢ã«ã¿ã¤ã ã«ã§ããªãããä½é¨ãè¯ãããã¾ããã ãã®ãããå°å³æ å ±ã®ç·¨éãã¼ã«ã¨ãã£ããã¬ãã¥ã¼ã®è¡¨ç¤ºã¯ã¯ã©ã¤ã¢ã³ãå´ã§ãå°ç空éãã¼ã¿å¦çããã¦ãæä½å 容ã峿ã«åæ ã§ããã¨ä½é¨ãåä¸ããããã¨ãã§ãã¾ãã
ããããè¡ãã«ã¯ããã©ã¦ã¶ã§åä½ããå°ç空éãã¼ã¿å¦çãè¡ãã©ã¤ãã©ãªãå¿ è¦ã§ãã
ãã©ã¦ã¶ã§åä½ããå°ç空éãã¼ã¿å¦çã©ã¤ãã©ãªã¨ã㦠DuckDB-wasm ã使ã
ãã©ã¦ã¶ã§å°ç空éãã¼ã¿å¦çãè¡ãã©ã¤ãã©ãªã¨ãã¦ä½ã使ãããæ¤è¨ãã¦ãæçµçã«DuckDB-wasmã使ããã¨ã«ãã¾ããã
DuckDB-wasmã¯ãååã®éãDuckDBã® WebAssembly ãã«ãã§ãã ãã®ãDuckDB-wasm ã¯ãã©ã¦ã¶ã Node.js ãªã© Wasm ãå®è¡ã§ããç°å¢ã§ãDuckDB ãåãããã¨ãã§ãã¾ãã
DuckDB ã«ã¯Spatial Extensionã¨ãããã®ããããå°ç空éãã¼ã¿å¦çãæ±ãã¾ãã
ãã¼ã¿ãã¼ã¹ã§ã¯ãPostgres æ¡å¼µã®PostGISã SQLite æ¡å¼µã®SpatiaLiteãªã©ãããã¾ãã DuckDB ã®Spatial Extensionãããã¨åæ§ã®å°ç空éãã¼ã¿å¦çãæ±ãæ¡å¼µã§ãé¢ç½ãç¹ã¨ã㦠WebAssembly ã§åãç¹ãããããã¾ãã
PostGIS 㨠DuckDB ã® Spatial Extension ãããã¾ãã«æ¯è¼ãã¦ã¿ãã¨æ¬¡ã®ããã«ãªãã¾ãã ã¾ããJavaScriptã§å®è£ ãããå°ç空éãã¼ã¿å¦çã©ã¤ãã©ãªã§ãã Turf.js ã¨ã®æ¯è¼ãä½µãã¦ã¿ã¾ãã
| åºæ¬çãªCategory | DuckDB WASM | PostGIS | Turf |
|---|---|---|---|
| åºæ¬ãã¼ã¿å | GEOMETRY, POINT_2D, LINESTRING_2D, POLYGON_2D | geometry, geography | Point, LineString, Polygonãªã© |
| ç©ºéæ¼ç® | ST_Area, ST_Distance, ST_Union | ST_Area, ST_Distance, ST_Union, ST_Buffer | area, distance, union, buffer |
| 空éé¢ä¿ | ST_Contains, ST_Within, ST_Touches | ST_Contains, ST_Within, ST_Touches, ST_Crosses | booleanContains, booleanWithin, booleanOverlap |
| 座æ¨å¤æ | ST_Transform | ST_Transform, ST_SetSRID | transform |
| ã¸ãªã¡ããªçæ | ST_MakePoint, ST_MakeLine | ST_MakePoint, ST_MakeLine, ST_MakePolygon | point, lineString, polygon |
| éè¨é¢æ° | ST_Extent, ST_Union | ST_Extent, ST_Union, ST_Collect | collect, combine |
| ã¯ã©ã¹ã¿ãªã³ã° | ãªã | ST_ClusterDBSCAN, ST_ClusterKMeans | clustersKmeans |
| 測å°ç·è¨ç® | ST_Distance_Spheroid | ST_Distance_Spheroid, ST_Length_Spheroid | geodesicDistance |
| GeoJSON å¨ãã®æ¯è¼ | DuckDB WASM | PostGIS | Turf |
|---|---|---|---|
| GeoJSON 夿 | ST_AsGeoJSON | ST_AsGeoJSON | feature, featureCollection |
| ã¸ãªã¡ã㪠â GeoJSON | SELECT ST_AsGeoJSON(geom) | SELECT ST_AsGeoJSON(geom) | turf.feature(geometry) |
| ããããã£ä»ã Feature | json extension | ST_AsGeoJSON(t.*) | turf.feature(geometry, properties) |
| FeatureCollection çæ | json extension | json_build_object('type','FeatureCollection','features',json_agg(ST_AsGeoJSON(t.*)::json)) | turf.featureCollection(features)Â |
| CRS æå® | ST_Transform + ST_AsGeoJSON | ST_Transform + ST_AsGeoJSON1 | ãªãï¼WGS84 åºå®ï¼ |
| ãªãã·ã§ã³ | åºæ¬ç㪠GeoJSON åºåã®ã¿ | maxdecimaldigits, bbox, CRS æå®ãªã© | bbox, id æå®å¯è½ |
| åºæ¬ä½æ | ST_MakePolygon, ST_Polygon | ST_MakePolygon, ST_Polygon, ST_PolygonFromText | polygon, multiPolygon |
| Polygon æä½ | DuckDB WASM | PostGIS | Turf |
|---|---|---|---|
| ããªã´ã³æ¼ç® | ST_Union, ST_Intersection, ST_Difference | ST_Union, ST_Intersection, ST_Difference, ST_3DUnion | union, intersect, difference |
| 空éåæ | ST_Area, ST_Perimeter | ST_Area, ST_Perimeter, ST_3DArea, ST_3DPerimeter | area, perimeter |
| 空éé¢ä¿ | ST_Contains, ST_Within, ST_Overlaps7 | ST_Contains, ST_Within, ST_Overlaps, ST_3DIntersects | booleanContains, booleanWithin, booleanOverlap |
| æ¤è¨¼ | ST_IsValid, ST_IsSimple | ST_IsValid, ST_IsSimple, ST_IsValidReason | isPolygon, isMultiPolygon |
| 夿 | ST_Transform | ST_Transform, ST_Force3D | transformScale, transformRotate |
| åç´å颿° | ST_Simplify | ST_Simplify | simplify,polygonSmooth |
| ãµãã¼ããã¦ãããã¼ã¿å½¢å¼ | DuckDB WASM | PostGIS | Turf |
|---|---|---|---|
| å ¥åå½¢å¼ | GeoJSON, Shapefile, GeoPackage, KML, GML | GeoJSON, Shapefile, GeoPackage, KML, GML, WKT, WKB | GeoJSON ã®ã¿ |
| åºåå½¢å¼ | GeoJSON, WKT, WKB | GeoJSON, KML, SVG, WKT, WKB | GeoJSON ã®ã¿ |
| GeoJSON æä½ | ST_AsGeoJSON, ST_GeomFromGeoJSON | ST_AsGeoJSON, ST_GeomFromGeoJSON | feature, featureCollection10 |
| KML æä½ | ST_AsKML | ST_AsKML, ST_GeomFromKML | ãªã |
| WKT æä½ | ST_AsText, ST_GeomFromText | ST_AsText, ST_GeomFromText | ãªã |
| ãã¼ã¿èªã¿è¾¼ã¿ | ST_Read | ST_Read, ST_AsBinary | JSON.parse |
| ãã¼ã¿æ¸ãåºã | ST_AsGeoJSON, ST_AsText | ST_AsGeoJSON, ST_AsKML, ST_AsSVG | JSON.stringify |
ããã¾ãã«æ¯è¼ãã¦ããDuckDB ã® Spatial Extension 㯠PostGIS ã¨åçã®æ©è½æ§ãæã£ã¦ãããã¨ããããã¾ãã éã«Turf.jsã¯æ¼ç®ã®ã¿ãªã®ã§ããã¼ã¿å½¢å¼ã®å¤æãªã©ã«ã¤ãã¦ã¯ã¹ã³ã¼ãå¤ã¨ãªã£ã¦ãããã¨ããããã¾ãã
ãã©ã¦ã¶ä¸ã§åä½ããå°ç空éãã¼ã¿å¦çã©ã¤ãã©ãªã決ããããã«ããã®ãããªæ©è½æ¯è¼ãå®è£ ã¤ã¡ã¼ã¸ãã¦ã¼ã¹ã±ã¼ã¹ããµã¤ãºãã¡ãªãã/ãã¡ãªãããªã©ãæ¸ãã Design Doc ãæ¸ãã¦è°è«ãã¾ããã ãã®çµæãDuckDB-wasm ã使ããã¨ã«ãã¾ããã
ð¢ newmoã§ã® Design Doc ã«ã¤ãã¦
newmo ã®ããã³ãã¨ã³ãã§ã¯ã大ããªã©ã¤ãã©ãªãå°å ¥ããéã«ã¯ Design Doc ãæ¸ãã¦è°è«ãã¦ããæ±ºãããã¨ãå¤ãã§ãã ããã«ã¤ãã¦ã¯ã以忏ãã One Version Rule ãå®è·µããããã¨ããªããã®ã©ã¤ãã©ãªã使ã£ã¦ããã®ãã¨ããçµç·¯ã Design Doc(Architectural Decision Records ã¨ãã¦ã®å½¹å²)ã¨ãã¦æ®ãããã§ãã
ãã® Design Doc ã¨ã©ã¤ãã©ãªã®ç®¡çãªã©ã«ã¤ãã¦ã¯ãyui_tang ã 2024å¹´11æ23æ¥(åæ) ã®JSConf JPã§çºè¡¨ããäºå®ã§ãã
DuckDB-wasm + TypeScript ã§ SQL ã管çãã
å°ç空éãã¼ã¿å¦çã©ã¤ãã©ãªã¨ãã¦DuckDB-wasmã使ããã¨ã«ãã¾ããã ããã¯ãã¯ã©ã¤ã¢ã³ããµã¤ãã§ SQL ãæ¸ãã¦ãã¯ã©ã¤ã¢ã³ããµã¤ãã®Wasmä¸ã§ SQL ãå®è¡ããå¿ è¦ãããã¨ãããã¨ãæå³ãã¦ãã¾ãã
ä»å㯠DuckDB ã®ãã¼ã¿ãæ°¸ç¶åã¯ããã« In-Memory DB ã¨ãã¦å©ç¨ãã¦ããã®ã§ããã¤ã°ã¬ã¼ã·ã§ã³ã®ãããªè¤éãªåé¡ã¯ããã¾ãããã ããã§ã SQL ã管çããæ¹æ³ã¯èããå¿ è¦ãããã¾ãã
DuckDB-wasmã¯ã¾ã æ°ããã©ã¤ãã©ãªã§ãããããã©ã®ããã«æ±ããã®ãã¹ããã©ã¯ãã£ã¹ã確ç«ããã¦ãã¾ããã ãã®ãããä»å DuckDB-wasm ãæ±ãã«ããã£ã¦ãDuckDB ã§å®è¡ãã SQL ã TypeScript ã§ç®¡çããä»çµã¿ãä½ãã¾ããã
ä»çµã¿ã¨è¨ã£ã¦ãåç´ã§ãDuckDB ã§å®è¡ãã SQL ã«åãã¤ãã¦å®ç¾©ãã Utility 颿°ãç¨æããã ãã§ãã
次ã®ãã㪠SQL ãå®è¡ã§ãã颿°ãå®ç¾©ã§ãã Utility 颿°ãæä¾ãã¦ãã¾ãã
defineQueryOne: ä¸ã¤ã®çµæãè¿ãã¯ã¨ãªãå®è¡ãã颿°ãå®ç¾©ããdefineQueryMany: è¤æ°ã®çµæãè¿ãã¯ã¨ãªãå®è¡ãã颿°ãå®ç¾©ããdefineQueryExec: çµæãè¿ããªãã¯ã¨ãªãå®è¡ãã颿°ãå®ç¾©ããtransformQuery: ã¯ã¨ãªã®å®è¡çµæã夿ãã¦ã夿ããçµæãè¿ãããã«ããã¯ã¨ãªã®ã©ããã¼
ã¾ããã©ã®é¢æ°ã第ä¸å¼æ°ã«DuckDBContextãåãåããDuckDBContext㯠DuckDB ã¨æ¥ç¶ããããã®æ
å ±ãæã£ã¦ãã¾ãã
defineQuery.ts: SQLã管çããUtility颿°ã®ã³ã¼ã(ã¯ãªãã¯ã§éã)
import type { AsyncDuckDB, AsyncDuckDBConnection } from "@duckdb/duckdb-wasm"; /** * æ§æã¨ã©ã¼ãªã®ã§ã¯ã¨ãªã®æ¸ãæ¹ã®åé¡ããã */ export type DuckDBParserError = { type: "DuckDBParserError"; message: string; query: string; cause: Error; }; /** * 夿ã¨ã©ã¼ãªã®ã§ãã¼ã¿ã®åé¡ããã */ export type DuckDBConversionError = { type: "DuckDBConversionError"; message: string; query: string; cause: Error; }; /** * ãã¼ã¿ããªãå ´åã®ã¨ã©ã¼ */ export type DuckDBNoRowError = { type: "DuckDBNoRowError"; message: string; query: string; cause: Error; }; /** * ãã®ä»ã®ã¨ã©ã¼ */ export type DuckDBUnknownError = { type: "DuckDBUnknownError"; message: string; query: string; cause: Error; }; export type DuckDBSQLError = | DuckDBParserError | DuckDBConversionError | DuckDBNoRowError | DuckDBUnknownError; /** * DuckDBã®ã¯ã¨ãªã«æ¸¡ãContext */ export type DuckDBContext = { db: AsyncDuckDB; conn: AsyncDuckDBConnection; }; /** * DuckDBã®ã¨ã©ã¼ãã¨ã©ã¼ãªãã¸ã§ã¯ãã«ãã */ export const translateDuckDbError = ({ message, query, error, }: { message: string; query: string; error: unknown; }): DuckDBSQLError => { if (error instanceof Error) { if (error.message.includes("Parser Error")) { return { type: "DuckDBParserError", message, query, cause: error, }; } if (error.message.includes("Conversion Error")) { return { type: "DuckDBConversionError", message, query, cause: error, }; } } return { type: "DuckDBUnknownError", message, query, cause: error as Error, }; }; // Inputã {} ã®å ´åã¯ã Inputãoptionalã«ãã // keyof {} 㯠never ã«ãªãã®ãå©ç¨ãã¦å¤å®ãã¦ãã // https://stackoverflow.com/questions/62403425/conditional-type-for-empty-objects export type QueryFunction<Input, Output> = keyof Input extends never ? QueryFunctionWithOptionalArgs<Input, Output> : QueryFunctionWithArgsRequiredArgs<Input, Output>; export type QueryFunctionWithArgsRequiredArgs<Input, Output> = ( context: DuckDBContext, args: Input ) => Promise< | { ok: true; data: Output; } | { ok: false; errors: DuckDBSQLError[]; } >; export type QueryFunctionWithOptionalArgs<Input, Output> = ( context: DuckDBContext, args?: Input ) => Promise< | { ok: true; data: Output; } | { ok: false; errors: DuckDBSQLError[]; } >; /** * ä¸ã¤ã®çµæãè¿ãã¯ã¨ãªãå®ç¾©ãã * ãã¼ã¿ãåå¨ããªãã¨ãã¯ãDuckDBNoRowErrorã®ã¨ã©ã¼ãè¿ã * @param name 颿°å * @param sql ã¯ã¨ãª * @example * ```ts * const selectId = defineFunction<{id: string}, { id: string }>({ * name: "selectId", * query: () => `SELECT * FROM table WHERE id = ${id`, * }); * const result = await selectId(context, { id: "1" }); * console.log(result.data.id); // => "1" * const notFound = await selectId(context, { id: "2" }); * console.log(notFound.ok); // => false * ``` * */ export const defineQueryOne = < /** * ã¯ã¨ãªã®å¼æ° * 弿°ããªãå ´å㯠{} ãæå®ãã */ Input, /** * ã¯ã¨ãªã®å®è¡çµæã§åå¾ã§ãããã¼ã¿å */ Output >({ name, sql, }: { name: string; sql: (args: Input) => string; }): QueryFunction<Input, Output> => { const fn = async (context: DuckDBContext, args?: Input) => { const query = `-- name: ${name} :one ${sql(args ?? ({} as Input))}`; try { const q = await context.conn.prepare(query); const resultTable = await q.query(args); const firstData = resultTable.toArray()[0]; if (!firstData) { return { ok: false, errors: [ { type: "DuckDBNoRowError", message: `No row found: ${name}`, query, }, ], }; } return { ok: true, // ããããã®ã¢ã¤ãã ã¯Plainãªãªãã¸ã§ã¯ãã§ã¯ãªãã®ã§ãspread syntaxã§non-enumerableãªããããã£ãè½ã¨ã // åã«ã¯å®ç¾©ããã¦ãªããçã®ããããã£ãã§ããã ã触ããªãããã«ãã // TODO: JSON.parse(JSON.stringify(firstData)) ãªãå ¨ã¦è½ã¨ããããããã©ã¼ãã³ã¹ãæªã data: { ...firstData }, }; } catch (error: unknown) { return { ok: false, errors: [ translateDuckDbError({ message: `Failed to query: ${name}`, query, error, }), ], }; } }; // nameã颿°åã«è¨å®ãã Object.defineProperty(fn, "name", { value: name, configurable: true }); return fn as QueryFunction<Input, Output>; }; /** * è¤æ°ã®çµæãè¿ãã¯ã¨ãªãå®ç¾©ãã * @param name 颿°å * @param sql ã¯ã¨ãª * @example * ```ts * const selectAll = defineFunction<{id: string}, { id: string }>({ * name: "selectAll", * query: "SELECT * FROM table", * }); * const result = await selectAll(context, {}); * console.log(result.data); // => [{ id: "1" }, { id: "2" }] * ``` */ export const defineQueryMany = < /** * ã¯ã¨ãªã®å¼æ° * 弿°ããªãå ´å㯠{} ãæå®ãã */ Input, /** * ã¯ã¨ãªã®å®è¡çµæã§åå¾ã§ãããã¼ã¿å(è¦ç´ ã®åãªã®ã§ã[]ã¯ä¸è¦) */ Output >({ name, sql, }: { name: string; sql: (args: Input) => string; }): QueryFunction<Input, Output[]> => { const fn = async (context: DuckDBContext, args?: Input) => { const query = `-- name: ${name} :many ${sql(args ?? ({} as Input))}`; try { const q = await context.conn.prepare(query); const resultTable = await q.query(args); return { ok: true, data: resultTable.toArray(), }; } catch (error: unknown) { return { ok: false, errors: [ translateDuckDbError({ message: `Failed to query: ${name}`, query, error, }), ], }; } }; // nameã颿°åã«è¨å®ãã Object.defineProperty(fn, "name", { value: name, configurable: true }); return fn as QueryFunction<Input, Output[]>; }; /** * çµæãè¿ããªãã¯ã¨ãªãå®ç¾©ãã * @param name 颿°å * @param sql ã¯ã¨ãª * @example * ```ts * const update = defineFunction<{id: string}, undefined>({ * name: "update", * query: "UPDATE table SET id = $id", * }); * const result = await update(context, { id: "1" }); * console.log(result.ok); // => true * console.log(result.data); // => undefined * ``` */ export const defineQueryExec = < /** * ã¯ã¨ãªã®å¼æ° * 弿°ããªãå ´å㯠{} ãæå®ãã */ Input >({ name, sql, }: { name: string; sql: (args: Input) => string; }): QueryFunction<Input, undefined> => { const fn = async (context: DuckDBContext, args?: Input) => { const query = `-- name: ${name} :exec ${sql(args ?? ({} as Input))}`; try { const q = await context.conn.prepare(query); await q.query(args); return { ok: true, data: undefined, }; } catch (error: unknown) { return { ok: false, errors: [ translateDuckDbError({ message: `Failed to query: ${name}`, query, error, }), ], }; } }; // nameã颿°åã«è¨å®ãã Object.defineProperty(fn, "name", { value: name, configurable: true }); return fn as QueryFunction<Input, undefined>; }; /** * ã¯ã¨ãªã®å®è¡çµæã夿ãã¦ã夿ããçµæãè¿ãããã«ããã¯ã¨ãªã®ã©ããã¼ * @example * ```ts * const selectId = defineQueryOne<{id: string}, { id: string }>({ * name: "selectId", * sql: ({ id }) => `SELECT * FROM table WHERE id = ${id}`, * }); * const selectIdWithTransformed = transformQuery(selectId, (data) => { * return { * id: Number(data.id), * } * }); * const result = await selectIdWithTransformed(context, { id: "1" }); * console.log(result.data.id); // => 1 */ export const transformQuery = <TransformOutput, Input, Output>( query: QueryFunction<Input, Output>, transformFn: (data: Output) => TransformOutput ): QueryFunction<Input, TransformOutput> => { const fn = async (context: DuckDBContext, args?: Input) => { const result = await query(context, args ?? ({} as Input)); if (!result.ok) { return result; } return { ok: true, data: transformFn(result.data), }; }; // nameã颿°åã«è¨å®ãã Object.defineProperty(fn, "name", { value: `${query.name}WithTransformed`, configurable: true, }); return fn as QueryFunction<Input, TransformOutput>; };
ãããã® Utility 颿°ã使ã£ã¦ã次ã®ããã« SQL ãå®è¡ãã颿°ãå®ç¾©ã§ãã¾ãã
import { defineQueryOne, defineQueryMany, defineQueryExec } from "./defineQuery.ts"; /** * DuckDBã«Spatialæ¡å¼µãã¤ã³ã¹ãã¼ã«ããã¯ã¨ãª */ export const installSpatialExtension = defineQueryExec({ name: "installSpatialExtension", sql: () => ` INSTALL spatial; LOAD spatial; `, }); /** * ãã¼ãã«ã使ããã¯ã¨ãª */ export const createTable = defineQueryExec({ name: "createTable", sql: () => ` CREATE TABLE table ( id STRING name STRING ); `, }); /** * idãæå®ãã¦ãã¼ã¿ãåå¾ããã¯ã¨ãª */ export const selectId = defineQueryOne< { id: string }, { id: string; name: string } >({ name: "selectId", sql: ({ id }) => `SELECT * FROM table WHERE id = ${id}`, }); /** * å ¨ã¦ã®ãã¼ã¿ãåå¾ããã¯ã¨ãª */ export const selectAll = defineQueryMany<{}, { id: string; name: string }>({ name: "selectAll", sql: () => `SELECT * FROM table`, }); /** * ãã¼ã¿ãæ¿å ¥ããã¯ã¨ãª */ export const insert = defineQueryExec<{ id: string; name: string }>({ name: "insert", sql: ({ id, name }) => `INSERT INTO table VALUES (${id}, ${name})`, });
ãããã®ã¯ã¨ãªã¯æ¬¡ã®ããã«å®è¡ã§ãã¾ãã DuckDB-wasm ã®ä½¿ãæ¹ã«ã¤ãã¦ã¯ãå ¬å¼ããã¥ã¡ã³ããåç §ãã¦ãã ããã
import * as duckdb from "@duckdb/duckdb-wasm"; import duckdb_wasm from "@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm"; import duckdb_wasm_next from "@duckdb/duckdb-wasm/dist/duckdb-eh.wasm"; import { installSpatialExtension } from "./sql.ts"; const setupDuckDBForBrowser = async () => { const MANUAL_BUNDLES: duckdb.DuckDBBundles = { mvp: { mainModule: duckdb_wasm, mainWorker: new URL( "@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js", import.meta.url ).toString(), }, eh: { mainModule: duckdb_wasm_next, mainWorker: new URL( "@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js", import.meta.url ).toString(), }, }; const bundle = await duckdb.selectBundle(MANUAL_BUNDLES); const worker = new Worker(bundle.mainWorker!); const logger = new duckdb.ConsoleLogger(); const db = new duckdb.AsyncDuckDB(logger, worker); await db.instantiate(bundle.mainModule, bundle.pthreadWorker); const conn = await db.connect(); const duckDBContext = { conn, db, }; const installedResult = await installSpatialExtension(duckDBContext); if (!installedResult.ok) { throw new Error("Failed to install spatial extension", { cause: installedResult.errors, }); } return duckDBContext; }; const duckDBContext = await setupDuckDBForBrowser(); // insert const insertResult = await insert(duckDBContext, { id: "1", name: "name" }); if (!insertResult.ok) { console.error("Failed to insert", insertResult.errors); return; } // select const selectResult = await selectId(duckDBContext, { id: insertResult.data.id, }); if (!selectResult.ok) { console.error("Failed to select", selectResult.errors); return; } console.log("select", selectResult.data); // => { id: "1", name: "name" } // select all const selectAllResult = await selectAll(duckDBContext); if (!selectAllResult.ok) { console.error("Failed to select all", selectAllResult.errors); return; } console.log("select all", selectAllResult.data); // => [{ id: "1", name: "name" }]
ãã®defineQuery*颿°ãªã©ã®Utilityã¯200-300è¡ç¨åº¦ã®å°ããªUtilityã§ãããã¯ã¨ãªãå®ç¾©ããå´ã¯ã·ã³ãã«ã«SQLãæ¸ãã ãã§è¯ããªãã¾ãã
SQLã®Inputã¨Outputã¯TypeScriptã§åå®ç¾©ãããã¨ã§ãã¯ã¨ãªãå®è¡ããå´ã¯åå®å
¨ã«ã¯ã¨ãªãå®è¡ã§ãã¾ãã
ã¾ããã¯ã¨ãªã®å®è¡çµæã¯{ ok: true, data: Output }ã¾ãã¯{ ok: false, errors: DuckDBSQLError[] }ã¨ããResultåã®ãããªå¤ãè¿ãããã«ãªã£ã¦ãã¾ãã
ããã¯ãã¨ã©ã¼ãå¤ã¨ãã¦è¿ããã»ããåå®å
¨ã«ã¨ã©ã¼ãã³ããªã³ã°ãæ¸ããããããã§ãã
DuckDB-wasm ã®ãã¹ããNode.jsã§åãã
DuckDB-wasm ã¯ãã©ã¦ã¶ã§åä½ããã©ã¤ãã©ãªã§ãããWebAssemblyãªã®ã§Node.jsã§ãåãããã¨ãã§ãã¾ãã DuckDBã§è¡ãå¦çã¯ç¹ã«ãã©ã¦ã¶ã«ä¾åã¯ãã¦ããªãã®ã§ãNode.jsã§åãã¨ãã¹ããç°¡åã«åãããããã«ãªãã¾ãã
Node.jsåãã®å
¬å¼ã®ããã¥ã¡ã³ããã¾ã æ´åããã¦ããªãã®ã§ãåèç¨åº¦ã«ãªãã¾ãããæ¬¡ã®ããã«Node.jsã§ãDuckDB-wasmã使ããã¨ãã§ãã¾ãã
次ã®ãã¹ãã³ã¼ãã§ã¯ã"@duckdb/duckdb-wasm/blocking"ã使ã£ã¦ãNode.jsã§Blocking APIã®DuckDBã¤ã³ã¹ã¿ã³ã¹ãåããã¦ãã¾ãã
web-workerãªã©ã®Node.jsåãã®Web Worker APIã使ãã¨ããã©ã¦ã¶ã¨åãéåæAPIã®DuckDBã使ããã¨ãã§ãã¾ãã
ãã ãä½è¨ãªã©ã¤ãã©ãªãå¿
è¦ã ã£ããããã¹ãç®çãªãBlocking APIã§ããã¾ãå°ããªãã£ãã®ã§ã"@duckdb/duckdb-wasm/blocking"ã使ã£ã¦ãã¾ãã
import { createDuckDB, NODE_RUNTIME } from "@duckdb/duckdb-wasm/blocking"; import { createRequire } from "module"; import { dirname, resolve } from "path"; import * as duckdb from "@duckdb/duckdb-wasm"; import { defineQueryExec, defineQueryMany, defineQueryOne, type DuckDBSQLError, type DuckDBContext, transformQuery, } from "./defineQuery.ts"; import { describe, it, expect } from "vitest"; const require = createRequire(import.meta.url); const DUCKDB_DIST = dirname(require.resolve("@duckdb/duckdb-wasm")); /** * create initialized duckDB for Node.js * @returns {Promise<void>} */ export async function setupDuckDBForNodejs(): Promise<DuckDBContext> { const DUCKDB_BUNDLES = { mvp: { mainModule: resolve(DUCKDB_DIST, "./duckdb-mvp.wasm"), mainWorker: resolve(DUCKDB_DIST, "./duckdb-node-mvp.worker.cjs"), }, eh: { mainModule: resolve(DUCKDB_DIST, "./duckdb-eh.wasm"), mainWorker: resolve(DUCKDB_DIST, "./duckdb-node-eh.worker.cjs"), }, }; // SyncDBã¨ãã¦ä½æãã¦ãã¾ã // .thenã.catchãªã©ã使ããªããã°ãç¹ã«éãã¯æèããªãã¦ãã // TODO: Syncã¯å ¬å¼ã«ãµãã¼ãããã¦ãããAsyncã¯WebWorkerã«ä¾åãã¦ãããã工夫ãå¿ è¦ // https://github.com/duckdb/duckdb-wasm/blob/6fcc50318b3a0e6b4e30c78bfdda19b9f86f4012/packages/duckdb-wasm/test/index_node.ts#L56 const logger = new duckdb.ConsoleLogger(); const db = await createDuckDB(DUCKDB_BUNDLES, logger, NODE_RUNTIME); await db.instantiate(); const conn = db.connect(); // @ts-expect-error -- syncã®ãã®ãasyncã¨ãã¦æ¸¡ãã¦ãããã const duckDBContext = { db, conn, } as DuckDBContext; // spatial extensionãã¤ã³ã¹ãã¼ã« const installResult = await installSpatialExtension(duckDBContext); if (!installResult.ok) { throw new Error("Failed to install spatial extension", { cause: installResult.errors, }); } return duckDBContext; } /** * ã¯ã¨ãªã®å®è¡çµæãæåãã¦ãããAssertionãã * 失æãã¦ãæã®ãã°ãåºåãã * @param result */ export function assertQueryResultOk(result: { ok: boolean; errors?: any[]; }): asserts result is { ok: true } { if (!result.ok) { const error = new Error( "Assertion failed: query result is not ok. expected result.ok is true", ); console.error(error, { errors: result.errors, }); throw error; } } const createTestTable = defineQueryExec<{}>({ name: "createTestTable", sql: () => ` CREATE TABLE test_table ( id UUID PRIMARY KEY DEFAULT uuid(), name TEXT ) `, }); const insertTestItem = defineQueryOne< { name: string }, { id: string; } >({ name: "insertTestItem", sql: ({ name }) => ` INSERT INTO test_table (name) VALUES ('${name}') RETURNING id `, }); const getTestItem = defineQueryOne< { id: string }, { id: string; name: string } >({ name: "getTestItem", sql: ({ id }) => ` SELECT id, name FROM test_table WHERE id = '${id}' `, }); describe("DuckDB Utils", () => { describe("defineQueryOne", () => { it("should return one result", async () => { const duckDBContext = await setupDuckDBForNodejs(); assertQueryResultOk(await createTestTable(duckDBContext)); const insertResult = await insertTestItem(duckDBContext, { name: "test", }); assertQueryResultOk(insertResult); const insertedId = insertResult.data.id; const result = await getTestItem(duckDBContext, { id: insertedId, }); assertQueryResultOk(result); expect(result.data).toEqual({ id: insertedId, name: "test" }); }); }); });
ããã§å®ç¾©ããã¯ã¨ãªã®ãã¹ããNode.jsã§ãåãããã®ã§ãUnit Testãªã©ãç°¡åã«æ¸ããããã«ãªã£ã¦ãã¾ãã
ä»å¾ã®å±æ
defineQuery*颿°ã§çºè¡ãããSQLã¯ã-- name: ${name} :oneã®ãããªå½¢å¼ã³ã¡ã³ããå
¥ãã¦ãããã¨ã«æ°ã¥ãã人ãããããããã¾ããã
ããã¯sqlcãæèãã¦ä½ã£ãä»çµã¿ã§ãããããdefineQuery*颿°ãããããsqlcã®Query annotationsã«å¯¾å¿ããå½¢ã§ä½æãã¦ãã¾ãã
sqlcã¯ãSQLãæ¸ãã¦Goã®ã³ã¼ããTypeScriptã®ã³ã¼ããçæã§ãããã¼ã«ã§ãã ç¾ç¶ã®sqlcã¯DuckDBã«ã¯å¯¾å¿ãã¦ãã¾ãããå°æ¥çã«ã¯ãSQLãæ¸ãã¦ãã®ã¯ã¨ãªãå®è¡ã§ããã³ã¼ããçæãããããªä»çµã¿ã«ç½®ãæãããã¨ãæ¤è¨ãã¦ãã¾ãã
ç¾ç¶ã®Utilityã¯Inputã¨Outputã®åå®ç¾©ãå®å ¨ã«æåã§ããããããã®ãã¼ã«ã対å¿ãããã¨DBã®ã¹ãã¼ãããTypeScriptã§åå®ç¾©ãçæã§ããããã¦ããå¹ççã«éçºã§ããããã«ãªãããããã¾ããã
ä»åç´¹ä»ããå®è£ ã§ã¯ãSQLã®ã¨ã¹ã±ã¼ããprepared statementã¯ç¹ã«å¯¾å¿ãæ¸ãã¦ãã¾ããã ããã¯ãå®è¡ããSQLã®å¯¾è±¡ããã©ã¦ã¶ä¸ã®ä¸æçãªè¨ç®ã®ããã®ãã¼ã¿ã§ãæ¼ããã夿´ãã¦ãåé¡ãªããã¼ã¿ã§ããããã§ã(ããã¾ã§ãã¼ã¿ã¯ãã®ãã©ã¦ã¶å ã®å¤ã§ããã¼ã¸å ã«éãã¦ãã¾ã)ã ã¾ã DuckDB Wasmã®prepared statementã®æåãã¾ã ããããé¨åããããããã§ããã ãã·ã³ãã«ãªä»çµã¿ã«ãããã£ãã®ãããã¾ãã
- [DuckDB Wasm] How to use named prepared statements? · duckdb/duckdb · Discussion #11782
- Support array arguments in prepared statements · Issue #447 · duckdb/duckdb-wasm
ãããããä»å¾prepared statementã®å¯¾å¿ã@vercel/postgresã®ãããªTagged Functionã使ã£ãã¨ã¹ã±ã¼ããå¿ è¦ã«ãªãããããã¾ããã ãã®ããããã®è¨äºã®ã³ã¼ããå©ç¨ããå ´åã¯ããã®ç¹ã«çæãã¦ãã ããã
ã¾ããdefineQuery*颿°ã¨ã¯å¥ã«ã¯ã¨ãªã®å®è¡çµæã夿ã§ããtransformQuery颿°ãæä¾ãã¦ãã¾ãã
ã¯ã¨ãªãå®è¡ã§ãã颿°ã®å®ç¾©ã¨å¤æå¦çãåããã®ã¯ãå°æ¥çã«ã¯ã¯ã¨ãªãå®è¡ã§ãã颿°ã¯èªåçæããå¯è½æ§ãããã¨æã£ãããã§ãã
ã¯ã¨ãªã®å®ç¾©ã¨å¤æå¦çãåãã¦ãããã¨ã§ãã¯ã¨ãªã®å®ç¾©ã ããèªåçæãããããªä»çµã¿ãä½ãããããªãã¾ãã
ãã®ãããdefineQuery*颿°ã®ä¸ã«ã¯ãã¯ã¨ãªã®å®è¡çµæããã¾ã夿ããå¦çã¯å
¥ããªãããã«ãã¦ãã¦ãã·ã³ãã«ãªå®è¡çµæãè¿ãã ãã®é¢æ°ã«ãã¦ãã¾ãã
ã¾ã¨ã
DuckDBã®WebAssemblyçã§ããDuckDB-wasmã使ã£ã¦ããã©ã¦ã¶ä¸ã§å°ç空éãã¼ã¿å¦çãããSQLã®ç®¡çãããä»çµã¿ãä½ãã¾ããã
å°ããªä»çµã¿ã§ãããSQLã¯SQLã¨ãã¦ããç¨åº¦ç¬ç«ãããã®ã¨ãã¦å®ç¾©ã§ããããã«ãªããåå®ç¾©ãæç¤ºçã«æ¸ãå¿ è¦ãããã®ã§TypeScriptãããæ±ãããããªã£ãã¨æãã¾ãã ç§©åºãªãã¢ããªã±ã¼ã·ã§ã³ã®ã³ã¼ãã®SQLããã¿ãããã¦ãã¾ãã¨ãå¾ãã夿´ãã§ããªããªã£ã¦ãã¾ãã¾ãã å°æ¥çã«ã¯ãSQLããã³ã¼ãçæããã¦ããã£ã¨å®å ¨ã§æ¥½ã«DuckDB-wasmã使ããããªä»çµã¿ãä½ããã¨ãæ¤è¨ãã¦ãã¾ãã
宣ä¼
2024å¹´11æ16æ¥(åæ)ã«éå¬ãããTSKaigi Kansai 2024ã§ããã©ã¦ã¶ã§å®çµï¼DuckDB Wasmã§ã¿ã¯ã·ã¼å°å³æ å ±ãå¯è¦åã¨ããã¿ã¤ãã«ã§ã¹ãã³ãµã¼LTãããã®ã§ããã²è´ãã«æ¥ã¦ãã ããï¼
ã¾ããã¹ãã³ãµã¼ãã¼ã¹ã§ã¯ãDuckDB-wasmã®é¸å®ã«ä½¿ã£ãDesign Docããä»åã®SQL管çã®ä»çµã¿ãè°è«ããDesign Docãªã©ãå±ç¤ºããäºå®ã§ã
newmoã§ã¯å°çæ å ±ã·ã¹ãã ï¼GISï¼Geographic Information Systemï¼ã«èå³ã®ããã¨ã³ã¸ãã¢ãç©æ¥µçã«æ¡ç¨ä¸ã§ãï¼