├── .gitignore ├── LICENSE ├── README.md ├── jest.config.json ├── package-lock.json ├── package.json ├── src └── duckdb-async.ts ├── test ├── __snapshots__ │ └── basic.test.ts.snap ├── basic.test.ts └── support │ └── script.sql └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | *.map 4 | .DS_Store 5 | npm-debug.log 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 MotherDuck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # duckdb-async 2 | 3 | TypeScript wrappers using Promises for the duckdb Node.JS API 4 | 5 | # Overview and Basic Usage 6 | 7 | This repository provides an API that wraps the [DuckDb NodeJS API](https://duckdb.org/docs/api/nodejs/overview) using Promises 8 | instead of callbacks. 9 | The library is implemented in TypeScript to provide static type checking for TypeScript developers. It includes the existing `duckdb` 10 | NPM module as a dependency, so it should be possible to write applications in TypeScript using only `duckdb-async` as a direct dependency. 11 | 12 | Basic usage is straightforward. For example: 13 | 14 | ```typescript 15 | import { Database } from "duckdb-async"; 16 | 17 | async function simpleTest() { 18 | const db = await Database.create(":memory:"); 19 | const rows = await db.all("select * from range(1,10)"); 20 | console.log(rows); 21 | } 22 | 23 | simpleTest(); 24 | ``` 25 | 26 | Note that the static method `Database.create(...)` is used in place of `new Database(...)` in the DuckDb NodeJS API 27 | because the underlying NodeJS API uses a callback in the constructor, and it's not possible to have constructors 28 | return promises. 29 | 30 | The API should be relatively complete -- there are wrappers for all of the `Connection`, `Database` and `Statement` 31 | classes from the underlying NodeJS API, with methods that return promises instead of taking callbacks. 32 | A notable exception is the `each` methods on these classes. The `each` method invokes a callback multiple times, once 33 | for each row of the result set. Since promises can only be resolved once, it doesn't make sense to convert this 34 | method to a promise-based API, so the `each` method still provides the same callback-based interface as the 35 | original Node.JS API. 36 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.tsx?$": "ts-jest" 4 | }, 5 | "testRegex": "/test/.*test.ts", 6 | "testEnvironment": "node", 7 | "testPathIgnorePatterns": ["/node_modules/", "/__snapshots__"], 8 | "moduleFileExtensions": ["ts", "js"], 9 | "testTimeout": 15000 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duckdb-async", 3 | "version": "1.2.1", 4 | "description": "Promise wrappers for DuckDb NodeJS API", 5 | "main": "dist/duckdb-async.js", 6 | "types": "dist/duckdb-async.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "jest --config jest.config.json --no-cache" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/motherduckdb/duckdb-async.git" 14 | }, 15 | "keywords": [ 16 | "duckdb", 17 | "database", 18 | "typescript", 19 | "promise" 20 | ], 21 | "author": "", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/motherduckdb/duckdb-async/issues" 25 | }, 26 | "homepage": "https://github.com/motherduckdb/duckdb-async#readme", 27 | "dependencies": { 28 | "duckdb": "^1.2.1" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^29.2.0", 32 | "@types/node": "^18.11.0", 33 | "jest": "^29.2.1", 34 | "ts-jest": "^29.0.3", 35 | "typescript": "^4.8.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/duckdb-async.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A wrapper around DuckDb node.js API that mirrors that 3 | * API but uses Promises instead of callbacks. 4 | * 5 | */ 6 | import * as duckdb from "duckdb"; 7 | import { ColumnInfo, TypeInfo } from "duckdb"; 8 | import * as util from "util"; 9 | 10 | type Callback = (err: duckdb.DuckDbError | null, res: T) => void; 11 | 12 | export { 13 | DuckDbError, 14 | QueryResult, 15 | RowData, 16 | TableData, 17 | OPEN_CREATE, 18 | OPEN_FULLMUTEX, 19 | OPEN_PRIVATECACHE, 20 | OPEN_READONLY, 21 | OPEN_READWRITE, 22 | OPEN_SHAREDCACHE, 23 | } from "duckdb"; 24 | 25 | /* 26 | * Implmentation note: 27 | * Although the method types exposed to users of this library 28 | * are reasonably precise, the unfortunate excessive use of 29 | * `any` in this utility function is because writing a precise 30 | * type for a generic higher order function like 31 | * `util.promisify` is beyond the current capabilities of the 32 | * TypeScript type system. 33 | * See https://github.com/Microsoft/TypeScript/issues/5453 34 | * for detailed discussion. 35 | */ 36 | function methodPromisify( 37 | methodFn: (...args: any[]) => any 38 | ): (target: T, ...args: any[]) => Promise { 39 | return util.promisify((target: T, ...args: any[]): any => 40 | methodFn.bind(target)(...args) 41 | ) as any; 42 | } 43 | 44 | const connAllAsync = methodPromisify( 45 | duckdb.Connection.prototype.all 46 | ); 47 | 48 | const connArrowIPCAll = methodPromisify( 49 | duckdb.Connection.prototype.arrowIPCAll 50 | ); 51 | 52 | const connExecAsync = methodPromisify( 53 | duckdb.Connection.prototype.exec 54 | ); 55 | 56 | const connPrepareAsync = methodPromisify( 57 | duckdb.Connection.prototype.prepare 58 | ); 59 | 60 | const connRunAsync = methodPromisify( 61 | duckdb.Connection.prototype.run 62 | ); 63 | 64 | const connUnregisterUdfAsync = methodPromisify( 65 | duckdb.Connection.prototype.unregister_udf 66 | ); 67 | 68 | const connRegisterBufferAsync = methodPromisify( 69 | duckdb.Connection.prototype.register_buffer 70 | ); 71 | 72 | const connUnregisterBufferAsync = methodPromisify( 73 | duckdb.Connection.prototype.unregister_buffer 74 | ); 75 | 76 | const connCloseAsync = methodPromisify( 77 | duckdb.Connection.prototype.close 78 | ); 79 | 80 | export class Connection { 81 | private conn: duckdb.Connection | null = null; 82 | 83 | private constructor( 84 | ddb: duckdb.Database, 85 | resolve: (c: Connection) => void, 86 | reject: (reason: any) => void 87 | ) { 88 | this.conn = new duckdb.Connection(ddb, (err, res: any) => { 89 | if (err) { 90 | this.conn = null; 91 | reject(err); 92 | } 93 | resolve(this); 94 | }); 95 | } 96 | 97 | /** 98 | * Static method to create a new Connection object. Provided because constructors can not return Promises, 99 | * and the DuckDb Node.JS API uses a callback in the Database constructor 100 | */ 101 | static create(db: Database): Promise { 102 | return new Promise((resolve, reject) => { 103 | new Connection(db.get_ddb_internal(), resolve, reject); 104 | }); 105 | } 106 | 107 | async all(sql: string, ...args: any[]): Promise { 108 | if (!this.conn) { 109 | throw new Error("Connection.all: uninitialized connection"); 110 | } 111 | return connAllAsync(this.conn, sql, ...args); 112 | } 113 | 114 | async arrowIPCAll(sql: string, ...args: any[]): Promise { 115 | if (!this.conn) { 116 | throw new Error("Connection.arrowIPCAll: uninitialized connection"); 117 | } 118 | return connArrowIPCAll(this.conn, sql, ...args); 119 | } 120 | 121 | /** 122 | * Executes the sql query and invokes the callback for each row of result data. 123 | * Since promises can only resolve once, this method uses the same callback 124 | * based API of the underlying DuckDb NodeJS API 125 | * @param sql query to execute 126 | * @param args parameters for template query 127 | * @returns 128 | */ 129 | each(sql: string, ...args: [...any, Callback] | []): void { 130 | if (!this.conn) { 131 | throw new Error("Connection.each: uninitialized connection"); 132 | } 133 | this.conn.each(sql, ...args); 134 | } 135 | 136 | /** 137 | * Execute one or more SQL statements, without returning results. 138 | * @param sql queries or statements to executes (semicolon separated) 139 | * @param args parameters if `sql` is a parameterized template 140 | * @returns `Promise` that resolves when all statements have been executed. 141 | */ 142 | async exec(sql: string, ...args: any[]): Promise { 143 | if (!this.conn) { 144 | throw new Error("Connection.exec: uninitialized connection"); 145 | } 146 | return connExecAsync(this.conn, sql, ...args); 147 | } 148 | 149 | prepareSync(sql: string, ...args: any[]): Statement { 150 | if (!this.conn) { 151 | throw new Error("Connection.prepareSync: uninitialized connection"); 152 | } 153 | const ddbStmt = this.conn.prepare(sql, ...(args as any)); 154 | return Statement.create_internal(ddbStmt); 155 | } 156 | 157 | async prepare(sql: string, ...args: any[]): Promise { 158 | if (!this.conn) { 159 | throw new Error("Connection.prepare: uninitialized connection"); 160 | } 161 | const stmt = await connPrepareAsync(this.conn, sql, ...args); 162 | return Statement.create_internal(stmt); 163 | } 164 | 165 | runSync(sql: string, ...args: any[]): Statement { 166 | if (!this.conn) { 167 | throw new Error("Connection.runSync: uninitialized connection"); 168 | } 169 | // We need the 'as any' cast here, because run dynamically checks 170 | // types of args to determine if a callback function was passed in 171 | const ddbStmt = this.conn.run(sql, ...(args as any)); 172 | return Statement.create_internal(ddbStmt); 173 | } 174 | 175 | async run(sql: string, ...args: any[]): Promise { 176 | if (!this.conn) { 177 | throw new Error("Connection.runSync: uninitialized connection"); 178 | } 179 | const stmt = await connRunAsync(this.conn, sql, ...args); 180 | return Statement.create_internal(stmt); 181 | } 182 | 183 | register_udf( 184 | name: string, 185 | return_type: string, 186 | fun: (...args: any[]) => any 187 | ): void { 188 | if (!this.conn) { 189 | throw new Error("Connection.register_udf: uninitialized connection"); 190 | } 191 | this.conn.register_udf(name, return_type, fun); 192 | } 193 | async unregister_udf(name: string): Promise { 194 | if (!this.conn) { 195 | throw new Error("Connection.unregister_udf: uninitialized connection"); 196 | } 197 | return connUnregisterUdfAsync(this.conn, name); 198 | } 199 | register_bulk( 200 | name: string, 201 | return_type: string, 202 | fun: (...args: any[]) => any 203 | ): void { 204 | if (!this.conn) { 205 | throw new Error("Connection.register_bulk: uninitialized connection"); 206 | } 207 | this.conn.register_bulk(name, return_type, fun); 208 | } 209 | 210 | stream(sql: any, ...args: any[]): duckdb.QueryResult { 211 | if (!this.conn) { 212 | throw new Error("Connection.stream: uninitialized connection"); 213 | } 214 | return this.conn.stream(sql, ...args); 215 | } 216 | 217 | arrowIPCStream( 218 | sql: any, 219 | ...args: any[] 220 | ): Promise { 221 | if (!this.conn) { 222 | throw new Error("Connection.arrowIPCStream: uninitialized connection"); 223 | } 224 | return this.conn.arrowIPCStream(sql, ...args); 225 | } 226 | 227 | register_buffer( 228 | name: string, 229 | array: duckdb.ArrowIterable, 230 | force: boolean 231 | ): Promise { 232 | if (!this.conn) { 233 | throw new Error("Connection.register_buffer: uninitialized connection"); 234 | } 235 | return connRegisterBufferAsync(this.conn, name, array, force); 236 | } 237 | 238 | unregister_buffer(name: string): Promise { 239 | if (!this.conn) { 240 | throw new Error("Connection.unregister_buffer: uninitialized connection"); 241 | } 242 | return connUnregisterBufferAsync(this.conn, name); 243 | } 244 | 245 | async close(): Promise { 246 | if (!this.conn) { 247 | throw new Error("Connection.close: uninitialized connection"); 248 | } 249 | await connCloseAsync(this.conn); 250 | this.conn = null; 251 | return; 252 | } 253 | } 254 | 255 | const dbCloseAsync = methodPromisify( 256 | duckdb.Database.prototype.close 257 | ); 258 | const dbAllAsync = methodPromisify( 259 | duckdb.Database.prototype.all 260 | ); 261 | const dbArrowIPCAll = methodPromisify( 262 | duckdb.Database.prototype.arrowIPCAll 263 | ); 264 | 265 | const dbExecAsync = methodPromisify( 266 | duckdb.Database.prototype.exec 267 | ); 268 | 269 | const dbPrepareAsync = methodPromisify( 270 | duckdb.Database.prototype.prepare 271 | ); 272 | 273 | const dbRunAsync = methodPromisify( 274 | duckdb.Database.prototype.run 275 | ); 276 | 277 | const dbUnregisterUdfAsync = methodPromisify( 278 | duckdb.Database.prototype.unregister_udf 279 | ); 280 | 281 | const dbSerializeAsync = methodPromisify( 282 | duckdb.Database.prototype.serialize 283 | ); 284 | 285 | const dbParallelizeAsync = methodPromisify( 286 | duckdb.Database.prototype.parallelize 287 | ); 288 | 289 | const dbWaitAsync = methodPromisify( 290 | duckdb.Database.prototype.wait 291 | ); 292 | 293 | const dbRegisterBufferAsync = methodPromisify( 294 | duckdb.Database.prototype.register_buffer 295 | ); 296 | 297 | const dbUnregisterBufferAsync = methodPromisify( 298 | duckdb.Database.prototype.unregister_buffer 299 | ); 300 | 301 | export class Database { 302 | private db: duckdb.Database | null = null; 303 | 304 | private constructor( 305 | path: string, 306 | accessMode: number | Record, 307 | resolve: (db: Database) => void, 308 | reject: (reason: any) => void 309 | ) { 310 | if (typeof accessMode === "number") { 311 | accessMode = { 312 | access_mode: accessMode == duckdb.OPEN_READONLY ? "read_only" : "read_write" 313 | }; 314 | } 315 | accessMode["duckdb_api"] = "nodejs-async"; 316 | 317 | this.db = new duckdb.Database(path, accessMode, (err, res) => { 318 | if (err) { 319 | reject(err); 320 | } 321 | resolve(this); 322 | }); 323 | } 324 | 325 | /** 326 | * Static method to create a new Database object. Provided because constructors can not return Promises, 327 | * and the DuckDb Node.JS API uses a callback in the Database constructor 328 | */ 329 | 330 | /** 331 | * Static method to create a new Database object from the specified file. Provided as a static 332 | * method because some initialization may happen asynchronously. 333 | * @param path path to database file to open, or ":memory:" 334 | * @returns a promise that resolves to newly created Database object 335 | */ 336 | static create( 337 | path: string, 338 | accessMode?: number | Record 339 | ): Promise { 340 | const trueAccessMode = accessMode ?? duckdb.OPEN_READWRITE; // defaults to OPEN_READWRITE 341 | return new Promise((resolve, reject) => { 342 | new Database(path, trueAccessMode, resolve, reject); 343 | }); 344 | } 345 | 346 | async close(): Promise { 347 | if (!this.db) { 348 | throw new Error("Database.close: uninitialized database"); 349 | } 350 | await dbCloseAsync(this.db); 351 | this.db = null; 352 | return; 353 | } 354 | 355 | // accessor to get internal duckdb Database object -- internal use only 356 | get_ddb_internal(): duckdb.Database { 357 | if (!this.db) { 358 | throw new Error("Database.get_ddb_internal: uninitialized database"); 359 | } 360 | return this.db; 361 | } 362 | 363 | connect(): Promise { 364 | return Connection.create(this); 365 | } 366 | 367 | async all(sql: string, ...args: any[]): Promise { 368 | if (!this.db) { 369 | throw new Error("Database.all: uninitialized database"); 370 | } 371 | return dbAllAsync(this.db, sql, ...args); 372 | } 373 | 374 | async arrowIPCAll(sql: string, ...args: any[]): Promise { 375 | if (!this.db) { 376 | throw new Error("Database.arrowIPCAll: uninitialized connection"); 377 | } 378 | return dbArrowIPCAll(this.db, sql, ...args); 379 | } 380 | 381 | /** 382 | * Executes the sql query and invokes the callback for each row of result data. 383 | * Since promises can only resolve once, this method uses the same callback 384 | * based API of the underlying DuckDb NodeJS API 385 | * @param sql query to execute 386 | * @param args parameters for template query 387 | * @returns 388 | */ 389 | each(sql: string, ...args: [...any, Callback] | []): void { 390 | if (!this.db) { 391 | throw new Error("Database.each: uninitialized database"); 392 | } 393 | this.db.each(sql, ...args); 394 | } 395 | 396 | /** 397 | * Execute one or more SQL statements, without returning results. 398 | * @param sql queries or statements to executes (semicolon separated) 399 | * @param args parameters if `sql` is a parameterized template 400 | * @returns `Promise` that resolves when all statements have been executed. 401 | */ 402 | async exec(sql: string, ...args: any[]): Promise { 403 | if (!this.db) { 404 | throw new Error("Database.exec: uninitialized database"); 405 | } 406 | return dbExecAsync(this.db, sql, ...args); 407 | } 408 | 409 | prepareSync(sql: string, ...args: any[]): Statement { 410 | if (!this.db) { 411 | throw new Error("Database.prepareSync: uninitialized database"); 412 | } 413 | const ddbStmt = this.db.prepare(sql, ...(args as any)); 414 | return Statement.create_internal(ddbStmt); 415 | } 416 | 417 | async prepare(sql: string, ...args: any[]): Promise { 418 | if (!this.db) { 419 | throw new Error("Database.prepare: uninitialized database"); 420 | } 421 | const stmt = await dbPrepareAsync(this.db, sql, ...args); 422 | return Statement.create_internal(stmt); 423 | } 424 | 425 | runSync(sql: string, ...args: any[]): Statement { 426 | if (!this.db) { 427 | throw new Error("Database.runSync: uninitialized database"); 428 | } 429 | // We need the 'as any' cast here, because run dynamically checks 430 | // types of args to determine if a callback function was passed in 431 | const ddbStmt = this.db.run(sql, ...(args as any)); 432 | return Statement.create_internal(ddbStmt); 433 | } 434 | 435 | async run(sql: string, ...args: any[]): Promise { 436 | if (!this.db) { 437 | throw new Error("Database.runSync: uninitialized database"); 438 | } 439 | const stmt = await dbRunAsync(this.db, sql, ...args); 440 | return Statement.create_internal(stmt); 441 | } 442 | 443 | register_udf( 444 | name: string, 445 | return_type: string, 446 | fun: (...args: any[]) => any 447 | ): void { 448 | if (!this.db) { 449 | throw new Error("Database.register: uninitialized database"); 450 | } 451 | this.db.register_udf(name, return_type, fun); 452 | } 453 | async unregister_udf(name: string): Promise { 454 | if (!this.db) { 455 | throw new Error("Database.unregister: uninitialized database"); 456 | } 457 | return dbUnregisterUdfAsync(this.db, name); 458 | } 459 | 460 | stream(sql: any, ...args: any[]): duckdb.QueryResult { 461 | if (!this.db) { 462 | throw new Error("Database.stream: uninitialized database"); 463 | } 464 | return this.db.stream(sql, ...args); 465 | } 466 | 467 | arrowIPCStream( 468 | sql: any, 469 | ...args: any[] 470 | ): Promise { 471 | if (!this.db) { 472 | throw new Error("Database.arrowIPCStream: uninitialized database"); 473 | } 474 | return this.db.arrowIPCStream(sql, ...args); 475 | } 476 | 477 | serialize(): Promise { 478 | if (!this.db) { 479 | throw new Error("Database.serialize: uninitialized database"); 480 | } 481 | return dbSerializeAsync(this.db); 482 | } 483 | 484 | parallelize(): Promise { 485 | if (!this.db) { 486 | throw new Error("Database.parallelize: uninitialized database"); 487 | } 488 | return dbParallelizeAsync(this.db); 489 | } 490 | 491 | wait(): Promise { 492 | if (!this.db) { 493 | throw new Error("Database.wait: uninitialized database"); 494 | } 495 | return dbWaitAsync(this.db); 496 | } 497 | 498 | interrupt(): void { 499 | if (!this.db) { 500 | throw new Error("Database.interrupt: uninitialized database"); 501 | } 502 | return this.db.interrupt(); 503 | } 504 | 505 | register_buffer( 506 | name: string, 507 | array: duckdb.ArrowIterable, 508 | force: boolean 509 | ): Promise { 510 | if (!this.db) { 511 | throw new Error("Database.register_buffer: uninitialized database"); 512 | } 513 | return dbRegisterBufferAsync(this.db, name, array, force); 514 | } 515 | 516 | unregister_buffer(name: string): Promise { 517 | if (!this.db) { 518 | throw new Error("Database.unregister_buffer: uninitialized database"); 519 | } 520 | return dbUnregisterBufferAsync(this.db, name); 521 | } 522 | 523 | registerReplacementScan( 524 | replacementScan: duckdb.ReplacementScanCallback 525 | ): Promise { 526 | if (!this.db) { 527 | throw new Error( 528 | "Database.registerReplacementScan: uninitialized database" 529 | ); 530 | } 531 | return this.db.registerReplacementScan(replacementScan); 532 | } 533 | } 534 | 535 | const stmtRunAsync = methodPromisify( 536 | duckdb.Statement.prototype.run 537 | ); 538 | 539 | const stmtFinalizeAsync = methodPromisify( 540 | duckdb.Statement.prototype.finalize 541 | ); 542 | 543 | const stmtAllAsync = methodPromisify( 544 | duckdb.Statement.prototype.all 545 | ); 546 | 547 | const stmtArrowIPCAllAsync = methodPromisify< 548 | duckdb.Statement, 549 | duckdb.ArrowArray 550 | >(duckdb.Statement.prototype.arrowIPCAll); 551 | 552 | export class Statement { 553 | private stmt: duckdb.Statement; 554 | 555 | /** 556 | * Construct an async wrapper from a statement 557 | */ 558 | private constructor(stmt: duckdb.Statement) { 559 | this.stmt = stmt; 560 | } 561 | 562 | /** 563 | * create a Statement object that wraps a duckdb.Statement. 564 | * This is intended for internal use only, and should not be called directly. 565 | * Use `Database.prepare()` or `Database.run()` to create Statement objects. 566 | */ 567 | static create_internal(stmt: duckdb.Statement): Statement { 568 | return new Statement(stmt); 569 | } 570 | 571 | async all(...args: any[]): Promise { 572 | return stmtAllAsync(this.stmt, ...args); 573 | } 574 | async arrowIPCAll(...args: any[]): Promise { 575 | return stmtArrowIPCAllAsync(this.stmt, ...args); 576 | } 577 | 578 | /** 579 | * Executes the sql query and invokes the callback for each row of result data. 580 | * Since promises can only resolve once, this method uses the same callback 581 | * based API of the underlying DuckDb NodeJS API 582 | * @param args parameters for template query, followed by a NodeJS style 583 | * callback function invoked for each result row. 584 | * 585 | * @returns 586 | */ 587 | each(...args: [...any, Callback] | []): void { 588 | this.stmt.each(...args); 589 | } 590 | 591 | /** 592 | * Call `duckdb.Statement.run` directly without awaiting completion. 593 | * @param args arguments passed to duckdb.Statement.run() 594 | * @returns this 595 | */ 596 | runSync(...args: any[]): Statement { 597 | this.stmt.run(...(args as any)); 598 | return this; 599 | } 600 | 601 | async run(...args: any[]): Promise { 602 | await stmtRunAsync(this.stmt, ...args); 603 | return this; 604 | } 605 | 606 | async finalize(): Promise { 607 | return stmtFinalizeAsync(this.stmt); 608 | } 609 | 610 | columns(): ColumnInfo[] { 611 | return this.stmt.columns(); 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /test/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Async API points Statement.columns 1`] = ` 4 | [ 5 | { 6 | "name": "bool", 7 | "type": { 8 | "id": "BOOLEAN", 9 | "sql_type": "BOOLEAN", 10 | }, 11 | }, 12 | { 13 | "name": "tinyint", 14 | "type": { 15 | "id": "TINYINT", 16 | "sql_type": "TINYINT", 17 | }, 18 | }, 19 | { 20 | "name": "smallint", 21 | "type": { 22 | "id": "SMALLINT", 23 | "sql_type": "SMALLINT", 24 | }, 25 | }, 26 | { 27 | "name": "int", 28 | "type": { 29 | "id": "INTEGER", 30 | "sql_type": "INTEGER", 31 | }, 32 | }, 33 | { 34 | "name": "bigint", 35 | "type": { 36 | "id": "BIGINT", 37 | "sql_type": "BIGINT", 38 | }, 39 | }, 40 | { 41 | "name": "hugeint", 42 | "type": { 43 | "id": "HUGEINT", 44 | "sql_type": "HUGEINT", 45 | }, 46 | }, 47 | { 48 | "name": "uhugeint", 49 | "type": { 50 | "id": "UHUGEINT", 51 | "sql_type": "UHUGEINT", 52 | }, 53 | }, 54 | { 55 | "name": "utinyint", 56 | "type": { 57 | "id": "UTINYINT", 58 | "sql_type": "UTINYINT", 59 | }, 60 | }, 61 | { 62 | "name": "usmallint", 63 | "type": { 64 | "id": "USMALLINT", 65 | "sql_type": "USMALLINT", 66 | }, 67 | }, 68 | { 69 | "name": "uint", 70 | "type": { 71 | "id": "UINTEGER", 72 | "sql_type": "UINTEGER", 73 | }, 74 | }, 75 | { 76 | "name": "ubigint", 77 | "type": { 78 | "id": "UBIGINT", 79 | "sql_type": "UBIGINT", 80 | }, 81 | }, 82 | { 83 | "name": "varint", 84 | "type": { 85 | "id": "VARINT", 86 | "sql_type": "VARINT", 87 | }, 88 | }, 89 | { 90 | "name": "date", 91 | "type": { 92 | "id": "DATE", 93 | "sql_type": "DATE", 94 | }, 95 | }, 96 | { 97 | "name": "time", 98 | "type": { 99 | "id": "TIME", 100 | "sql_type": "TIME", 101 | }, 102 | }, 103 | { 104 | "name": "timestamp", 105 | "type": { 106 | "id": "TIMESTAMP", 107 | "sql_type": "TIMESTAMP", 108 | }, 109 | }, 110 | { 111 | "name": "timestamp_s", 112 | "type": { 113 | "id": "TIMESTAMP_S", 114 | "sql_type": "TIMESTAMP_S", 115 | }, 116 | }, 117 | { 118 | "name": "timestamp_ms", 119 | "type": { 120 | "id": "TIMESTAMP_MS", 121 | "sql_type": "TIMESTAMP_MS", 122 | }, 123 | }, 124 | { 125 | "name": "timestamp_ns", 126 | "type": { 127 | "id": "TIMESTAMP_NS", 128 | "sql_type": "TIMESTAMP_NS", 129 | }, 130 | }, 131 | { 132 | "name": "time_tz", 133 | "type": { 134 | "id": "TIME WITH TIME ZONE", 135 | "sql_type": "TIME WITH TIME ZONE", 136 | }, 137 | }, 138 | { 139 | "name": "timestamp_tz", 140 | "type": { 141 | "id": "TIMESTAMP WITH TIME ZONE", 142 | "sql_type": "TIMESTAMP WITH TIME ZONE", 143 | }, 144 | }, 145 | { 146 | "name": "float", 147 | "type": { 148 | "id": "FLOAT", 149 | "sql_type": "FLOAT", 150 | }, 151 | }, 152 | { 153 | "name": "double", 154 | "type": { 155 | "id": "DOUBLE", 156 | "sql_type": "DOUBLE", 157 | }, 158 | }, 159 | { 160 | "name": "dec_4_1", 161 | "type": { 162 | "id": "DECIMAL", 163 | "scale": 1, 164 | "sql_type": "DECIMAL(4,1)", 165 | "width": 4, 166 | }, 167 | }, 168 | { 169 | "name": "dec_9_4", 170 | "type": { 171 | "id": "DECIMAL", 172 | "scale": 4, 173 | "sql_type": "DECIMAL(9,4)", 174 | "width": 9, 175 | }, 176 | }, 177 | { 178 | "name": "dec_18_6", 179 | "type": { 180 | "id": "DECIMAL", 181 | "scale": 6, 182 | "sql_type": "DECIMAL(18,6)", 183 | "width": 18, 184 | }, 185 | }, 186 | { 187 | "name": "dec38_10", 188 | "type": { 189 | "id": "DECIMAL", 190 | "scale": 10, 191 | "sql_type": "DECIMAL(38,10)", 192 | "width": 38, 193 | }, 194 | }, 195 | { 196 | "name": "uuid", 197 | "type": { 198 | "id": "UUID", 199 | "sql_type": "UUID", 200 | }, 201 | }, 202 | { 203 | "name": "interval", 204 | "type": { 205 | "id": "INTERVAL", 206 | "sql_type": "INTERVAL", 207 | }, 208 | }, 209 | { 210 | "name": "varchar", 211 | "type": { 212 | "id": "VARCHAR", 213 | "sql_type": "VARCHAR", 214 | }, 215 | }, 216 | { 217 | "name": "blob", 218 | "type": { 219 | "id": "BLOB", 220 | "sql_type": "BLOB", 221 | }, 222 | }, 223 | { 224 | "name": "bit", 225 | "type": { 226 | "id": "BIT", 227 | "sql_type": "BIT", 228 | }, 229 | }, 230 | { 231 | "name": "small_enum", 232 | "type": { 233 | "id": "ENUM", 234 | "sql_type": "ENUM('DUCK_DUCK_ENUM', 'GOOSE')", 235 | "values": [ 236 | "DUCK_DUCK_ENUM", 237 | "GOOSE", 238 | ], 239 | }, 240 | }, 241 | { 242 | "name": "int_array", 243 | "type": { 244 | "child": { 245 | "id": "INTEGER", 246 | "sql_type": "INTEGER", 247 | }, 248 | "id": "LIST", 249 | "sql_type": "INTEGER[]", 250 | }, 251 | }, 252 | { 253 | "name": "double_array", 254 | "type": { 255 | "child": { 256 | "id": "DOUBLE", 257 | "sql_type": "DOUBLE", 258 | }, 259 | "id": "LIST", 260 | "sql_type": "DOUBLE[]", 261 | }, 262 | }, 263 | { 264 | "name": "date_array", 265 | "type": { 266 | "child": { 267 | "id": "DATE", 268 | "sql_type": "DATE", 269 | }, 270 | "id": "LIST", 271 | "sql_type": "DATE[]", 272 | }, 273 | }, 274 | { 275 | "name": "timestamp_array", 276 | "type": { 277 | "child": { 278 | "id": "TIMESTAMP", 279 | "sql_type": "TIMESTAMP", 280 | }, 281 | "id": "LIST", 282 | "sql_type": "TIMESTAMP[]", 283 | }, 284 | }, 285 | { 286 | "name": "timestamptz_array", 287 | "type": { 288 | "child": { 289 | "id": "TIMESTAMP WITH TIME ZONE", 290 | "sql_type": "TIMESTAMP WITH TIME ZONE", 291 | }, 292 | "id": "LIST", 293 | "sql_type": "TIMESTAMP WITH TIME ZONE[]", 294 | }, 295 | }, 296 | { 297 | "name": "varchar_array", 298 | "type": { 299 | "child": { 300 | "id": "VARCHAR", 301 | "sql_type": "VARCHAR", 302 | }, 303 | "id": "LIST", 304 | "sql_type": "VARCHAR[]", 305 | }, 306 | }, 307 | { 308 | "name": "nested_int_array", 309 | "type": { 310 | "child": { 311 | "child": { 312 | "id": "INTEGER", 313 | "sql_type": "INTEGER", 314 | }, 315 | "id": "LIST", 316 | "sql_type": "INTEGER[]", 317 | }, 318 | "id": "LIST", 319 | "sql_type": "INTEGER[][]", 320 | }, 321 | }, 322 | { 323 | "name": "struct", 324 | "type": { 325 | "children": [ 326 | { 327 | "name": "a", 328 | "type": { 329 | "id": "INTEGER", 330 | "sql_type": "INTEGER", 331 | }, 332 | }, 333 | { 334 | "name": "b", 335 | "type": { 336 | "id": "VARCHAR", 337 | "sql_type": "VARCHAR", 338 | }, 339 | }, 340 | ], 341 | "id": "STRUCT", 342 | "sql_type": "STRUCT(a INTEGER, b VARCHAR)", 343 | }, 344 | }, 345 | { 346 | "name": "struct_of_arrays", 347 | "type": { 348 | "children": [ 349 | { 350 | "name": "a", 351 | "type": { 352 | "child": { 353 | "id": "INTEGER", 354 | "sql_type": "INTEGER", 355 | }, 356 | "id": "LIST", 357 | "sql_type": "INTEGER[]", 358 | }, 359 | }, 360 | { 361 | "name": "b", 362 | "type": { 363 | "child": { 364 | "id": "VARCHAR", 365 | "sql_type": "VARCHAR", 366 | }, 367 | "id": "LIST", 368 | "sql_type": "VARCHAR[]", 369 | }, 370 | }, 371 | ], 372 | "id": "STRUCT", 373 | "sql_type": "STRUCT(a INTEGER[], b VARCHAR[])", 374 | }, 375 | }, 376 | { 377 | "name": "array_of_structs", 378 | "type": { 379 | "child": { 380 | "children": [ 381 | { 382 | "name": "a", 383 | "type": { 384 | "id": "INTEGER", 385 | "sql_type": "INTEGER", 386 | }, 387 | }, 388 | { 389 | "name": "b", 390 | "type": { 391 | "id": "VARCHAR", 392 | "sql_type": "VARCHAR", 393 | }, 394 | }, 395 | ], 396 | "id": "STRUCT", 397 | "sql_type": "STRUCT(a INTEGER, b VARCHAR)", 398 | }, 399 | "id": "LIST", 400 | "sql_type": "STRUCT(a INTEGER, b VARCHAR)[]", 401 | }, 402 | }, 403 | { 404 | "name": "map", 405 | "type": { 406 | "id": "MAP", 407 | "key": { 408 | "id": "VARCHAR", 409 | "sql_type": "VARCHAR", 410 | }, 411 | "sql_type": "MAP(VARCHAR, VARCHAR)", 412 | "value": { 413 | "id": "VARCHAR", 414 | "sql_type": "VARCHAR", 415 | }, 416 | }, 417 | }, 418 | { 419 | "name": "union", 420 | "type": { 421 | "children": [ 422 | { 423 | "name": "name", 424 | "type": { 425 | "id": "VARCHAR", 426 | "sql_type": "VARCHAR", 427 | }, 428 | }, 429 | { 430 | "name": "age", 431 | "type": { 432 | "id": "SMALLINT", 433 | "sql_type": "SMALLINT", 434 | }, 435 | }, 436 | ], 437 | "id": "UNION", 438 | "sql_type": "UNION("name" VARCHAR, age SMALLINT)", 439 | }, 440 | }, 441 | { 442 | "name": "fixed_int_array", 443 | "type": { 444 | "id": "ARRAY", 445 | "sql_type": "INTEGER[3]", 446 | }, 447 | }, 448 | { 449 | "name": "fixed_varchar_array", 450 | "type": { 451 | "id": "ARRAY", 452 | "sql_type": "VARCHAR[3]", 453 | }, 454 | }, 455 | { 456 | "name": "fixed_nested_int_array", 457 | "type": { 458 | "id": "ARRAY", 459 | "sql_type": "INTEGER[3][3]", 460 | }, 461 | }, 462 | { 463 | "name": "fixed_nested_varchar_array", 464 | "type": { 465 | "id": "ARRAY", 466 | "sql_type": "VARCHAR[3][3]", 467 | }, 468 | }, 469 | { 470 | "name": "fixed_struct_array", 471 | "type": { 472 | "id": "ARRAY", 473 | "sql_type": "STRUCT(a INTEGER, b VARCHAR)[3]", 474 | }, 475 | }, 476 | { 477 | "name": "struct_of_fixed_array", 478 | "type": { 479 | "children": [ 480 | { 481 | "name": "a", 482 | "type": { 483 | "id": "ARRAY", 484 | "sql_type": "INTEGER[3]", 485 | }, 486 | }, 487 | { 488 | "name": "b", 489 | "type": { 490 | "id": "ARRAY", 491 | "sql_type": "VARCHAR[3]", 492 | }, 493 | }, 494 | ], 495 | "id": "STRUCT", 496 | "sql_type": "STRUCT(a INTEGER[3], b VARCHAR[3])", 497 | }, 498 | }, 499 | { 500 | "name": "fixed_array_of_int_list", 501 | "type": { 502 | "id": "ARRAY", 503 | "sql_type": "INTEGER[][3]", 504 | }, 505 | }, 506 | { 507 | "name": "list_of_fixed_int_array", 508 | "type": { 509 | "child": { 510 | "id": "ARRAY", 511 | "sql_type": "INTEGER[3]", 512 | }, 513 | "id": "LIST", 514 | "sql_type": "INTEGER[3][]", 515 | }, 516 | }, 517 | ] 518 | `; 519 | -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as duckdb from "../src/duckdb-async"; 2 | import { Database } from "../src/duckdb-async"; 3 | import fs from "fs"; 4 | 5 | test("t0 - basic database create", async () => { 6 | const db = await Database.create(":memory:"); 7 | expect(db).toBeInstanceOf(Database); 8 | }); 9 | 10 | describe("Async API points", () => { 11 | let db: Database; 12 | 13 | beforeAll(async () => { 14 | db = await Database.create(":memory:"); 15 | }); 16 | 17 | test("Database.create -- read only flag", async () => { 18 | try { 19 | const roDb = await Database.create(":memory:", duckdb.OPEN_READONLY); 20 | } catch (rawErr) { 21 | const err = rawErr as duckdb.DuckDbError; 22 | expect(err.message).toContain( 23 | "Cannot launch in-memory database in read-only mode!" 24 | ); 25 | } 26 | }); 27 | 28 | test("Database.create -- with record arguments", async () => { 29 | const rwOptsDb = await Database.create(":memory:", { 30 | access_mode: "READ_WRITE", 31 | max_memory: "512MB", 32 | threads: "4", 33 | }); 34 | const user_agent = await rwOptsDb.all("PRAGMA user_agent"); 35 | expect(user_agent[0]["user_agent"]).toMatch(/duckdb\/[^ ]* nodejs-async/); 36 | }); 37 | 38 | test("Database.create -- explicit numeric read/write flag", async () => { 39 | const rwDb = await Database.create(":memory:", duckdb.OPEN_READWRITE); 40 | await rwDb.exec("CREATE TABLE foo (txt text, num int, flt double, blb blob)"); 41 | const empty_result = await rwDb.all("SELECT * FROM foo"); 42 | expect(empty_result.length).toBe(0); 43 | const user_agent = await rwDb.all("PRAGMA user_agent"); 44 | expect(user_agent[0]["user_agent"]).toMatch(/duckdb\/[^ ]* nodejs-async/); 45 | }); 46 | 47 | test("Database.create -- user agent", async () => { 48 | const rwDb = await Database.create(":memory:"); 49 | const user_agent = await rwDb.all("PRAGMA user_agent"); 50 | expect(user_agent[0]["user_agent"]).toMatch(/duckdb\/[^ ]* nodejs-async/); 51 | }); 52 | 53 | test("Database.all -- basic query", async () => { 54 | const results = await db.all("select 42 as a"); 55 | expect(results.length).toBe(1); 56 | expect(results).toEqual([{ a: 42 }]); 57 | }); 58 | 59 | test("Database.all -- erroneous query", async () => { 60 | try { 61 | const results = await db.all("select * from bogusTable"); 62 | } catch (rawErr) { 63 | const err = rawErr as duckdb.DuckDbError; 64 | expect(err.message).toContain( 65 | "Table with name bogusTable does not exist!" 66 | ); 67 | expect(err.code).toBe("DUCKDB_NODEJS_ERROR"); 68 | } 69 | }); 70 | 71 | test("Database.exec -- multiple statements (and verify results)", async () => { 72 | var sql = fs.readFileSync("test/support/script.sql", "utf8"); 73 | await db.exec(sql); 74 | const rows = await db.all( 75 | "SELECT type, name FROM sqlite_master ORDER BY type, name" 76 | ); 77 | expect(rows).toEqual([ 78 | { type: "table", name: "grid_key" }, 79 | { type: "table", name: "grid_utfgrid" }, 80 | { type: "table", name: "images" }, 81 | { type: "table", name: "keymap" }, 82 | { type: "table", name: "map" }, 83 | { type: "table", name: "metadata" }, 84 | { type: "view", name: "grid_data" }, 85 | { type: "view", name: "grids" }, 86 | { type: "view", name: "tiles" }, 87 | ]); 88 | }); 89 | 90 | test("basic connect and Connection.all", async () => { 91 | const minVal = 1, 92 | maxVal = 10; 93 | 94 | const conn = await db.connect(); 95 | 96 | const rows = await conn.all("SELECT * from range(?,?)", minVal, maxVal); 97 | expect(rows).toEqual([ 98 | { range: 1n }, 99 | { range: 2n }, 100 | { range: 3n }, 101 | { range: 4n }, 102 | { range: 5n }, 103 | { range: 6n }, 104 | { range: 7n }, 105 | { range: 8n }, 106 | { range: 9n }, 107 | ]); 108 | }); 109 | 110 | test("basic connect and Connection.close", async () => { 111 | const minVal = 1, 112 | maxVal = 10; 113 | 114 | const conn = await db.connect(); 115 | 116 | const rows = await conn.all("SELECT * from range(?,?)", minVal, maxVal); 117 | expect(rows).toEqual([ 118 | { range: 1n }, 119 | { range: 2n }, 120 | { range: 3n }, 121 | { range: 4n }, 122 | { range: 5n }, 123 | { range: 6n }, 124 | { range: 7n }, 125 | { range: 8n }, 126 | { range: 9n }, 127 | ]); 128 | 129 | await conn.close(); 130 | 131 | try { 132 | const nextRows = await conn.all( 133 | "SELECT * from range(?,?)", 134 | minVal, 135 | maxVal 136 | ); 137 | } catch (rawErr) { 138 | const err = rawErr as duckdb.DuckDbError; 139 | expect(err.message).toContain("uninitialized connection"); 140 | } 141 | }); 142 | 143 | test("basic statement prepare/run/finalize", async () => { 144 | const stmt = await db.prepare( 145 | "CREATE TABLE foo (txt text, num int, flt double, blb blob)" 146 | ); 147 | await stmt.runSync().finalize(); 148 | }); 149 | 150 | test("Statement.all", async () => { 151 | const minVal = 1, 152 | maxVal = 10; 153 | const stmt = await db.prepare("SELECT * from range(?,?)"); 154 | const rows = await stmt.all(minVal, maxVal); 155 | expect(rows).toEqual([ 156 | { range: 1n }, 157 | { range: 2n }, 158 | { range: 3n }, 159 | { range: 4n }, 160 | { range: 5n }, 161 | { range: 6n }, 162 | { range: 7n }, 163 | { range: 8n }, 164 | { range: 9n }, 165 | ]); 166 | }); 167 | 168 | test("Statement.columns", async () => { 169 | const stmt = await db.prepare( 170 | "SELECT * EXCLUDE(medium_enum, large_enum) FROM test_all_types()" 171 | ); 172 | const cols = stmt.columns(); 173 | expect(cols).toMatchSnapshot(); 174 | }); 175 | 176 | test("prepareSync", async () => { 177 | await db 178 | .prepareSync("CREATE TABLE foo (txt text, num int, flt double, blb blob)") 179 | .runSync() 180 | .finalize(); 181 | }); 182 | 183 | test("ternary int udf", async () => { 184 | await db.register_udf( 185 | "udf", 186 | "integer", 187 | (x: number, y: number, z: number) => x + y + z 188 | ); 189 | const rows = await db.all("select udf(21, 20, 1) v"); 190 | expect(rows).toEqual([{ v: 42 }]); 191 | await db.unregister_udf("udf"); 192 | }); 193 | 194 | test("basic stream test", async () => { 195 | const total = 1000; 196 | 197 | let retrieved = 0; 198 | const conn = await db.connect(); 199 | const stream = conn.stream("SELECT * FROM range(0, ?)", total); 200 | for await (const row of stream) { 201 | retrieved++; 202 | } 203 | expect(total).toEqual(retrieved); 204 | }); 205 | 206 | test("arrowIPCAll", async () => { 207 | const range_size = 100; 208 | const query = `SELECT * FROM range(0,${range_size}) tbl(i)`; 209 | 210 | try { 211 | await db.all("INSTALL arrow"); 212 | await db.all("LOAD arrow"); 213 | const result = await db.arrowIPCAll(query); 214 | expect(result.length).toBe(3); 215 | 216 | const conn = await db.connect(); 217 | const cResult = await conn.arrowIPCAll(query); 218 | expect(cResult.length).toBe(3); 219 | } catch (err) { 220 | console.log("caught error: ", err); 221 | //expect((err as Error).message).toMatchInlineSnapshot(); 222 | } 223 | }); 224 | 225 | test("Database.close", async () => { 226 | await db.close(); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /test/support/script.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS map ( 2 | zoom_level INTEGER, 3 | tile_column INTEGER, 4 | tile_row INTEGER, 5 | tile_id TEXT, 6 | grid_id TEXT 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS grid_key ( 10 | grid_id TEXT, 11 | key_name TEXT 12 | ); 13 | 14 | CREATE TABLE IF NOT EXISTS keymap ( 15 | key_name TEXT, 16 | key_json TEXT 17 | ); 18 | 19 | CREATE TABLE IF NOT EXISTS grid_utfgrid ( 20 | grid_id TEXT, 21 | grid_utfgrid TEXT 22 | ); 23 | 24 | CREATE TABLE IF NOT EXISTS images ( 25 | tile_data blob, 26 | tile_id text 27 | ); 28 | 29 | CREATE TABLE IF NOT EXISTS metadata ( 30 | name text, 31 | value text 32 | ); 33 | 34 | 35 | -- CREATE UNIQUE INDEX IF NOT EXISTS map_index ON map (zoom_level, tile_column, tile_row); 36 | -- CREATE UNIQUE INDEX IF NOT EXISTS grid_key_lookup ON grid_key (grid_id, key_name); 37 | -- CREATE UNIQUE INDEX IF NOT EXISTS keymap_lookup ON keymap (key_name); 38 | -- CREATE UNIQUE INDEX IF NOT EXISTS grid_utfgrid_lookup ON grid_utfgrid (grid_id); 39 | -- CREATE UNIQUE INDEX IF NOT EXISTS images_id ON images (tile_id); 40 | -- CREATE UNIQUE INDEX IF NOT EXISTS name ON metadata (name); 41 | 42 | 43 | CREATE OR REPLACE VIEW tiles AS 44 | SELECT 45 | map.zoom_level AS zoom_level, 46 | map.tile_column AS tile_column, 47 | map.tile_row AS tile_row, 48 | images.tile_data AS tile_data 49 | FROM map 50 | JOIN images ON images.tile_id = map.tile_id; 51 | 52 | CREATE OR REPLACE VIEW grids AS 53 | SELECT 54 | map.zoom_level AS zoom_level, 55 | map.tile_column AS tile_column, 56 | map.tile_row AS tile_row, 57 | grid_utfgrid.grid_utfgrid AS grid 58 | FROM map 59 | JOIN grid_utfgrid ON grid_utfgrid.grid_id = map.grid_id; 60 | 61 | CREATE OR REPLACE VIEW grid_data AS 62 | SELECT 63 | map.zoom_level AS zoom_level, 64 | map.tile_column AS tile_column, 65 | map.tile_row AS tile_row, 66 | keymap.key_name AS key_name, 67 | keymap.key_json AS key_json 68 | FROM map 69 | JOIN grid_key ON map.grid_id = grid_key.grid_id 70 | JOIN keymap ON grid_key.key_name = keymap.key_name; 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["es2020", "esnext"], 6 | "module": "commonjs", 7 | "noImplicitAny": true, 8 | "outDir": "./dist", 9 | "strict": true, 10 | "target": "ES2020", 11 | "types": ["node", "jest"] 12 | }, 13 | "include": ["src/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | --------------------------------------------------------------------------------