duckdb 0.6.2-dev1060.0 → 0.6.2-dev1070.0

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/lib/duckdb.d.ts CHANGED
@@ -61,6 +61,15 @@ export class IpcResultStreamIterator implements AsyncIterator<Uint8Array>, Async
61
61
  toArray(): Promise<ArrowArray>;
62
62
  }
63
63
 
64
+ export interface ReplacementScanResult {
65
+ function: string;
66
+ parameters: Array<unknown>;
67
+ }
68
+
69
+ export type ReplacementScanCallback = (
70
+ table: string
71
+ ) => ReplacementScanResult | null;
72
+
64
73
  export class Database {
65
74
  constructor(path: string, accessMode?: number | Record<string,string>, callback?: Callback<any>);
66
75
  constructor(path: string, callback?: Callback<any>);
@@ -99,6 +108,10 @@ export class Database {
99
108
  register_buffer(name: string, array: ArrowIterable, force: boolean, callback?: Callback<void>): void;
100
109
 
101
110
  unregister_buffer(name: string, callback?: Callback<void>): void;
111
+
112
+ registerReplacementScan(
113
+ replacementScan: ReplacementScanCallback
114
+ ): Promise<void>;
102
115
  }
103
116
 
104
117
  export class Statement {
package/lib/duckdb.js CHANGED
@@ -609,6 +609,15 @@ Database.prototype.unregister_udf = function () {
609
609
  return this;
610
610
  }
611
611
 
612
+ /**
613
+ * Register a table replace scan function
614
+ * @method
615
+ * @arg fun Replacement scan function
616
+ * @return {this}
617
+ */
618
+
619
+ Database.prototype.registerReplacementScan;
620
+
612
621
  /**
613
622
  * Not implemented
614
623
  */
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "duckdb",
3
3
  "main": "./lib/duckdb.js",
4
4
  "types": "./lib/duckdb.d.ts",
5
- "version": "0.6.2-dev1060.0",
5
+ "version": "0.6.2-dev1070.0",
6
6
  "description": "DuckDB node.js API",
7
7
  "gypfile": true,
8
8
  "dependencies": {
@@ -26,6 +26,7 @@
26
26
  "test": "test"
27
27
  },
28
28
  "devDependencies": {
29
+ "@types/chai": "^4.3.4",
29
30
  "@types/mocha": "^10.0.0",
30
31
  "@types/node": "^18.11.0",
31
32
  "apache-arrow": "^9.0.0",
package/src/database.cpp CHANGED
@@ -1,7 +1,13 @@
1
- #include "duckdb_node.hpp"
1
+ #include "duckdb/parser/expression/constant_expression.hpp"
2
+ #include "duckdb/parser/expression/function_expression.hpp"
3
+ #include "duckdb/parser/tableref/table_function_ref.hpp"
2
4
  #include "duckdb/storage/buffer_manager.hpp"
5
+ #include "duckdb_node.hpp"
3
6
  #include "napi.h"
4
7
 
8
+ #include <iostream>
9
+ #include <thread>
10
+
5
11
  namespace node_duckdb {
6
12
 
7
13
  Napi::FunctionReference Database::constructor;
@@ -13,7 +19,8 @@ Napi::Object Database::Init(Napi::Env env, Napi::Object exports) {
13
19
  env, "Database",
14
20
  {InstanceMethod("close_internal", &Database::Close), InstanceMethod("wait", &Database::Wait),
15
21
  InstanceMethod("serialize", &Database::Serialize), InstanceMethod("parallelize", &Database::Parallelize),
16
- InstanceMethod("connect", &Database::Connect), InstanceMethod("interrupt", &Database::Interrupt)});
22
+ InstanceMethod("connect", &Database::Connect), InstanceMethod("interrupt", &Database::Interrupt),
23
+ InstanceMethod("registerReplacementScan", &Database::RegisterReplacementScan)});
17
24
 
18
25
  constructor = Napi::Persistent(t);
19
26
  constructor.SuppressDestruct();
@@ -267,4 +274,107 @@ Napi::Value Database::Connect(const Napi::CallbackInfo &info) {
267
274
  return Connection::constructor.New({Value()});
268
275
  }
269
276
 
277
+ struct JSRSArgs {
278
+ std::string table = "";
279
+ std::string function = "";
280
+ std::vector<duckdb::Value> parameters;
281
+ bool done = false;
282
+ duckdb::PreservedError error;
283
+ };
284
+
285
+ struct NodeReplacementScanData : duckdb::ReplacementScanData {
286
+ NodeReplacementScanData(duckdb_node_rs_function_t rs) : rs(std::move(rs)) {};
287
+ duckdb_node_rs_function_t rs;
288
+ };
289
+
290
+ void DuckDBNodeRSLauncher(Napi::Env env, Napi::Function jsrs, std::nullptr_t *, JSRSArgs *jsargs) {
291
+ try {
292
+ Napi::EscapableHandleScope scope(env);
293
+ auto arg = Napi::String::New(env, jsargs->table);
294
+ auto result = jsrs({arg});
295
+ if (result && result.IsObject()) {
296
+ auto obj = result.As<Napi::Object>();
297
+ jsargs->function = obj.Get("function").ToString().Utf8Value();
298
+ auto parameters = obj.Get("parameters");
299
+ if (parameters.IsArray()) {
300
+ auto paramArray = parameters.As<Napi::Array>();
301
+ for (uint32_t i = 0; i < paramArray.Length(); i++) {
302
+ jsargs->parameters.push_back(Utils::BindParameter(paramArray.Get(i)));
303
+ }
304
+ } else {
305
+ throw duckdb::Exception("Expected parameter array");
306
+ }
307
+ } else if (!result.IsNull()) {
308
+ throw duckdb::Exception("Invalid scan replacement result");
309
+ }
310
+ } catch (const duckdb::Exception &e) {
311
+ jsargs->error = duckdb::PreservedError(e);
312
+ } catch (const std::exception &e) {
313
+ jsargs->error = duckdb::PreservedError(e);
314
+ }
315
+ jsargs->done = true;
316
+ }
317
+
318
+ static duckdb::unique_ptr<duckdb::TableFunctionRef>
319
+ ScanReplacement(duckdb::ClientContext &context, const std::string &table_name, duckdb::ReplacementScanData *data) {
320
+ JSRSArgs jsargs;
321
+ jsargs.table = table_name;
322
+ ((NodeReplacementScanData *)data)->rs.BlockingCall(&jsargs);
323
+ while (!jsargs.done) {
324
+ std::this_thread::yield();
325
+ }
326
+ if (jsargs.error) {
327
+ jsargs.error.Throw();
328
+ }
329
+ if (jsargs.function != "") {
330
+ auto table_function = duckdb::make_unique<duckdb::TableFunctionRef>();
331
+ std::vector<std::unique_ptr<duckdb::ParsedExpression>> children;
332
+ for (auto &param : jsargs.parameters) {
333
+ children.push_back(duckdb::make_unique<duckdb::ConstantExpression>(std::move(param)));
334
+ }
335
+ table_function->function =
336
+ duckdb::make_unique<duckdb::FunctionExpression>(jsargs.function, std::move(children));
337
+ return table_function;
338
+ }
339
+ return nullptr;
340
+ }
341
+
342
+ struct RegisterRsTask : public Task {
343
+ RegisterRsTask(Database &database, duckdb_node_rs_function_t rs, Napi::Promise::Deferred deferred)
344
+ : Task(database), rs(std::move(rs)), deferred(deferred) {
345
+ }
346
+
347
+ void DoWork() override {
348
+ auto &database = Get<Database>();
349
+ if (database.database) {
350
+ database.database->instance->config.replacement_scans.emplace_back(
351
+ ScanReplacement, duckdb::make_unique<NodeReplacementScanData>(rs));
352
+ }
353
+ }
354
+
355
+ void DoCallback() override {
356
+ deferred.Resolve(deferred.Env().Undefined());
357
+ }
358
+
359
+ duckdb_node_rs_function_t rs;
360
+ Napi::Promise::Deferred deferred;
361
+ };
362
+
363
+ Napi::Value Database::RegisterReplacementScan(const Napi::CallbackInfo &info) {
364
+ auto env = info.Env();
365
+ auto deferred = Napi::Promise::Deferred::New(info.Env());
366
+ if (info.Length() < 1) {
367
+ Napi::TypeError::New(env, "Replacement scan callback expected").ThrowAsJavaScriptException();
368
+ return env.Null();
369
+ }
370
+ Napi::Function rs_callback = info[0].As<Napi::Function>();
371
+ auto rs = duckdb_node_rs_function_t::New(env, rs_callback, "duckdb_node_rs_" + (replacement_scan_count++), 0, 1,
372
+ nullptr, [](Napi::Env, void *, std::nullptr_t *ctx) {});
373
+ rs.Unref(env);
374
+
375
+ Schedule(info.Env(), duckdb::make_unique<RegisterRsTask>(*this, rs, deferred));
376
+
377
+ return deferred.Promise();
378
+ }
379
+
270
380
  } // namespace node_duckdb
@@ -58,7 +58,20 @@ timestamp_t ICUCalendarAdd::Operation(timestamp_t timestamp, interval_t interval
58
58
  calendar->setTime(udate, status);
59
59
 
60
60
  // Add interval fields from lowest to highest
61
- calendar->add(UCAL_MILLISECOND, interval.micros / Interval::MICROS_PER_MSEC, status);
61
+
62
+ // Break units apart to avoid overflow
63
+ auto remaining = interval.micros / Interval::MICROS_PER_MSEC;
64
+ calendar->add(UCAL_MILLISECOND, remaining % Interval::MSECS_PER_SEC, status);
65
+
66
+ remaining /= Interval::MSECS_PER_SEC;
67
+ calendar->add(UCAL_SECOND, remaining % Interval::SECS_PER_MINUTE, status);
68
+
69
+ remaining /= Interval::SECS_PER_MINUTE;
70
+ calendar->add(UCAL_MINUTE, remaining % Interval::MINS_PER_HOUR, status);
71
+
72
+ remaining /= Interval::MINS_PER_HOUR;
73
+ calendar->add(UCAL_HOUR, remaining, status);
74
+
62
75
  calendar->add(UCAL_DATE, interval.days, status);
63
76
  calendar->add(UCAL_MONTH, interval.months, status);
64
77
 
@@ -1,8 +1,8 @@
1
1
  #ifndef DUCKDB_VERSION
2
- #define DUCKDB_VERSION "0.6.2-dev1060"
2
+ #define DUCKDB_VERSION "0.6.2-dev1070"
3
3
  #endif
4
4
  #ifndef DUCKDB_SOURCE_ID
5
- #define DUCKDB_SOURCE_ID "b3f6a8f16d"
5
+ #define DUCKDB_SOURCE_ID "e0cff6a3dd"
6
6
  #endif
7
7
  #include "duckdb/function/table/system_functions.hpp"
8
8
  #include "duckdb/main/database.hpp"
@@ -56,6 +56,11 @@ struct Task {
56
56
 
57
57
  class Connection;
58
58
 
59
+ struct JSRSArgs;
60
+ void DuckDBNodeRSLauncher(Napi::Env env, Napi::Function jsrs, std::nullptr_t *, JSRSArgs *data);
61
+
62
+ typedef Napi::TypedThreadSafeFunction<std::nullptr_t, JSRSArgs, DuckDBNodeRSLauncher> duckdb_node_rs_function_t;
63
+
59
64
  class Database : public Napi::ObjectWrap<Database> {
60
65
  public:
61
66
  explicit Database(const Napi::CallbackInfo &info);
@@ -83,6 +88,7 @@ public:
83
88
  Napi::Value Parallelize(const Napi::CallbackInfo &info);
84
89
  Napi::Value Interrupt(const Napi::CallbackInfo &info);
85
90
  Napi::Value Close(const Napi::CallbackInfo &info);
91
+ Napi::Value RegisterReplacementScan(const Napi::CallbackInfo &info);
86
92
 
87
93
  public:
88
94
  constexpr static int DUCKDB_NODEJS_ERROR = -1;
@@ -97,6 +103,7 @@ private:
97
103
  static Napi::FunctionReference constructor;
98
104
  Napi::Env env;
99
105
  int64_t bytes_allocated = 0;
106
+ int replacement_scan_count = 0;
100
107
  };
101
108
 
102
109
  struct JSArgs;
@@ -199,6 +206,7 @@ public:
199
206
  auto obj = T::constructor.New(args);
200
207
  return Napi::ObjectWrap<T>::Unwrap(obj);
201
208
  }
209
+ static duckdb::Value BindParameter(const Napi::Value source);
202
210
  };
203
211
 
204
212
  Napi::Array EncodeDataChunk(Napi::Env env, duckdb::DataChunk &chunk, bool with_types, bool with_data);
package/src/statement.cpp CHANGED
@@ -85,51 +85,6 @@ Statement::~Statement() {
85
85
  connection_ref = nullptr;
86
86
  }
87
87
 
88
- // A Napi InstanceOf for Javascript Objects "Date" and "RegExp"
89
- static bool OtherInstanceOf(Napi::Object source, const char *object_type) {
90
- if (strcmp(object_type, "Date") == 0) {
91
- return source.InstanceOf(source.Env().Global().Get(object_type).As<Napi::Function>());
92
- } else if (strcmp(object_type, "RegExp") == 0) {
93
- return source.InstanceOf(source.Env().Global().Get(object_type).As<Napi::Function>());
94
- }
95
-
96
- return false;
97
- }
98
-
99
- static duckdb::Value BindParameter(const Napi::Value source) {
100
- if (source.IsString()) {
101
- return duckdb::Value(source.As<Napi::String>().Utf8Value());
102
- } else if (OtherInstanceOf(source.As<Napi::Object>(), "RegExp")) {
103
- return duckdb::Value(source.ToString().Utf8Value());
104
- } else if (source.IsNumber()) {
105
- if (Utils::OtherIsInt(source.As<Napi::Number>())) {
106
- return duckdb::Value::INTEGER(source.As<Napi::Number>().Int32Value());
107
- } else {
108
- return duckdb::Value::DOUBLE(source.As<Napi::Number>().DoubleValue());
109
- }
110
- } else if (source.IsBoolean()) {
111
- return duckdb::Value::BOOLEAN(source.As<Napi::Boolean>().Value());
112
- } else if (source.IsNull()) {
113
- return duckdb::Value();
114
- } else if (source.IsBuffer()) {
115
- Napi::Buffer<char> buffer = source.As<Napi::Buffer<char>>();
116
- return duckdb::Value::BLOB(std::string(buffer.Data(), buffer.Length()));
117
- #if (NAPI_VERSION > 4)
118
- } else if (source.IsDate()) {
119
- const auto micros = int64_t(source.As<Napi::Date>().ValueOf()) * duckdb::Interval::MICROS_PER_MSEC;
120
- if (micros % duckdb::Interval::MICROS_PER_DAY) {
121
- return duckdb::Value::TIMESTAMP(duckdb::timestamp_t(micros));
122
- } else {
123
- const auto days = int32_t(micros / duckdb::Interval::MICROS_PER_DAY);
124
- return duckdb::Value::DATE(duckdb::date_t(days));
125
- }
126
- #endif
127
- } else if (source.IsObject()) {
128
- return duckdb::Value(source.ToString().Utf8Value());
129
- }
130
- return duckdb::Value();
131
- }
132
-
133
88
  static Napi::Value convert_col_val(Napi::Env &env, duckdb::Value dval, duckdb::LogicalTypeId id) {
134
89
  Napi::Value value;
135
90
 
@@ -472,7 +427,7 @@ duckdb::unique_ptr<StatementParam> Statement::HandleArgs(const Napi::CallbackInf
472
427
  if (p.IsUndefined()) {
473
428
  continue;
474
429
  }
475
- params->params.push_back(BindParameter(p));
430
+ params->params.push_back(Utils::BindParameter(p));
476
431
  }
477
432
  return params;
478
433
  }
package/src/utils.cpp CHANGED
@@ -20,5 +20,47 @@ Napi::Value Utils::CreateError(Napi::Env env, std::string msg) {
20
20
 
21
21
  return obj;
22
22
  }
23
+ // A Napi InstanceOf for Javascript Objects "Date" and "RegExp"
24
+ static bool OtherInstanceOf(Napi::Object source, const char *object_type) {
25
+ if (strcmp(object_type, "Date") == 0) {
26
+ return source.InstanceOf(source.Env().Global().Get(object_type).As<Napi::Function>());
27
+ } else if (strcmp(object_type, "RegExp") == 0) {
28
+ return source.InstanceOf(source.Env().Global().Get(object_type).As<Napi::Function>());
29
+ }
23
30
 
31
+ return false;
32
+ }
33
+ duckdb::Value Utils::BindParameter(const Napi::Value source) {
34
+ if (source.IsString()) {
35
+ return duckdb::Value(source.As<Napi::String>().Utf8Value());
36
+ } else if (OtherInstanceOf(source.As<Napi::Object>(), "RegExp")) {
37
+ return duckdb::Value(source.ToString().Utf8Value());
38
+ } else if (source.IsNumber()) {
39
+ if (Utils::OtherIsInt(source.As<Napi::Number>())) {
40
+ return duckdb::Value::INTEGER(source.As<Napi::Number>().Int32Value());
41
+ } else {
42
+ return duckdb::Value::DOUBLE(source.As<Napi::Number>().DoubleValue());
43
+ }
44
+ } else if (source.IsBoolean()) {
45
+ return duckdb::Value::BOOLEAN(source.As<Napi::Boolean>().Value());
46
+ } else if (source.IsNull()) {
47
+ return duckdb::Value();
48
+ } else if (source.IsBuffer()) {
49
+ Napi::Buffer<char> buffer = source.As<Napi::Buffer<char>>();
50
+ return duckdb::Value::BLOB(std::string(buffer.Data(), buffer.Length()));
51
+ #if (NAPI_VERSION > 4)
52
+ } else if (source.IsDate()) {
53
+ const auto micros = int64_t(source.As<Napi::Date>().ValueOf()) * duckdb::Interval::MICROS_PER_MSEC;
54
+ if (micros % duckdb::Interval::MICROS_PER_DAY) {
55
+ return duckdb::Value::TIMESTAMP(duckdb::timestamp_t(micros));
56
+ } else {
57
+ const auto days = int32_t(micros / duckdb::Interval::MICROS_PER_DAY);
58
+ return duckdb::Value::DATE(duckdb::date_t(days));
59
+ }
60
+ #endif
61
+ } else if (source.IsObject()) {
62
+ return duckdb::Value(source.ToString().Utf8Value());
63
+ }
64
+ return duckdb::Value();
65
+ }
24
66
  } // namespace node_duckdb
@@ -0,0 +1,144 @@
1
+ import * as sqlite3 from "../lib/duckdb";
2
+ import type { TableData } from "../lib/duckdb";
3
+ import { expect } from "chai";
4
+
5
+ const replacementScan = (table: string) => {
6
+ if (table.endsWith(".csv")) {
7
+ return null;
8
+ } else {
9
+ return {
10
+ function: "read_csv_auto",
11
+ parameters: [`test/support/${table}.csv`],
12
+ };
13
+ }
14
+ };
15
+
16
+ const invalidTableFunction = (table: string) => {
17
+ return {
18
+ function: "foo",
19
+ parameters: ["bar"],
20
+ };
21
+ };
22
+
23
+ const invalidResultType = (table: string) => {
24
+ return "hello" as unknown as sqlite3.ReplacementScanResult;
25
+ };
26
+
27
+ const invalidResultKeys = (table: string) => {
28
+ return {
29
+ foo: "foo",
30
+ bar: "bar",
31
+ } as unknown as sqlite3.ReplacementScanResult;
32
+ };
33
+
34
+ describe("replacement scan", () => {
35
+ var db: sqlite3.Database;
36
+ describe("without replacement scan", () => {
37
+ before(function (done) {
38
+ db = new sqlite3.Database(":memory:", done);
39
+ });
40
+
41
+ it("is not found", (done) => {
42
+ db.all(
43
+ "SELECT * FROM 'prepare' LIMIT 5",
44
+ function (err: null | Error, rows: TableData) {
45
+ expect(err).not.to.be.null;
46
+ expect(err!.message).to.match(
47
+ /Table with name prepare does not exist/
48
+ );
49
+ done();
50
+ }
51
+ );
52
+ });
53
+ });
54
+
55
+ describe("with replacement scan", () => {
56
+ before((done) => {
57
+ db = new sqlite3.Database(":memory:", () => {
58
+ db.registerReplacementScan(replacementScan).then(done);
59
+ });
60
+ });
61
+
62
+ it("is found when pattern matches", (done) => {
63
+ db.all(
64
+ "SELECT * FROM 'prepare' LIMIT 5",
65
+ function (err: null | Error, rows: TableData) {
66
+ expect(rows.length).to.equal(5);
67
+ done();
68
+ }
69
+ );
70
+ });
71
+
72
+ it("handles null response", (done) => {
73
+ db.all(
74
+ "SELECT * FROM 'test/support/prepare.csv' LIMIT 5",
75
+ function (err: null | Error, rows: TableData) {
76
+ expect(rows.length).to.equal(5);
77
+ done();
78
+ }
79
+ );
80
+ });
81
+
82
+ it("errors with invalid table", (done) => {
83
+ db.all(
84
+ "SELECT * FROM 'missing' LIMIT 5",
85
+ function (err: null | Error, rows: TableData) {
86
+ expect(err).not.to.be.null;
87
+ expect(err!.message).to.match(
88
+ /No files found that match the pattern "test\/support\/missing.csv"/
89
+ );
90
+ done();
91
+ }
92
+ );
93
+ });
94
+ });
95
+
96
+ describe("with invalid replacement scan functions", () => {
97
+ it("does not crash with bad return values", (done) => {
98
+ db = new sqlite3.Database(":memory:", () => {
99
+ db.registerReplacementScan(invalidTableFunction).then(() => {
100
+ db.all(
101
+ "SELECT * FROM 'missing' LIMIT 5",
102
+ function (err: null | Error, rows: TableData) {
103
+ expect(err).not.to.be.null;
104
+ expect(err!.message).to.match(
105
+ /Table Function with name foo does not exist/
106
+ );
107
+ done();
108
+ }
109
+ );
110
+ });
111
+ });
112
+ });
113
+
114
+ it("does not crash with invalid response", (done) => {
115
+ db = new sqlite3.Database(":memory:", () => {
116
+ db.registerReplacementScan(invalidResultType).then(() => {
117
+ db.all(
118
+ "SELECT * FROM 'missing' LIMIT 5",
119
+ function (err: null | Error, rows: TableData) {
120
+ expect(err).not.to.be.null;
121
+ expect(err!.message).to.match(/Invalid scan replacement result/);
122
+ done();
123
+ }
124
+ );
125
+ });
126
+ });
127
+ });
128
+
129
+ it("does not crash with invalid response object", (done) => {
130
+ db = new sqlite3.Database(":memory:", () => {
131
+ db.registerReplacementScan(invalidResultKeys).then(() => {
132
+ db.all(
133
+ "SELECT * FROM 'missing' LIMIT 5",
134
+ function (err: null | Error, rows: TableData) {
135
+ expect(err).not.to.be.null;
136
+ expect(err!.message).to.match(/Expected parameter array/);
137
+ done();
138
+ }
139
+ );
140
+ });
141
+ });
142
+ });
143
+ });
144
+ });