@rip-lang/db 1.3.3 → 1.3.4
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/client.rip +51 -18
- package/db.rip +35 -1
- package/lib/duckdb.mjs +166 -24
- package/package.json +1 -1
package/client.rip
CHANGED
|
@@ -52,6 +52,15 @@ export query = (sql, params = []) ->
|
|
|
52
52
|
throw new Error data.error if data.error
|
|
53
53
|
data
|
|
54
54
|
|
|
55
|
+
export bulk = (table, columns, rows) ->
|
|
56
|
+
res = fetch! "#{dbUrl()}/sql",
|
|
57
|
+
method: 'POST'
|
|
58
|
+
headers: { 'Content-Type': 'application/json' }
|
|
59
|
+
body: JSON.stringify { table, columns, rows }
|
|
60
|
+
data = res.json!
|
|
61
|
+
throw new Error data.error if data.error
|
|
62
|
+
data
|
|
63
|
+
|
|
55
64
|
materialize = (meta, row) ->
|
|
56
65
|
obj = {}
|
|
57
66
|
for col, i in meta
|
|
@@ -258,11 +267,17 @@ export Model = (table, database = null) ->
|
|
|
258
267
|
findAll! sql, params
|
|
259
268
|
|
|
260
269
|
insert: (data) ->
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
270
|
+
if Array.isArray(data)
|
|
271
|
+
return { rows: 0 } if data.length is 0
|
|
272
|
+
columns = Object.keys(data[0]).filter (k) -> data[0][k] isnt undefined
|
|
273
|
+
rows = data.map (row) -> columns.map (k) -> row[k]
|
|
274
|
+
bulk! table, columns, rows
|
|
275
|
+
else
|
|
276
|
+
keys = Object.keys(data).filter (k) -> data[k] isnt undefined
|
|
277
|
+
cols = keys.map((k) -> "\"#{k}\"").join(', ')
|
|
278
|
+
refs = keys.map((_, i) -> "$#{i + 1}").join(', ')
|
|
279
|
+
vals = keys.map (k) -> data[k]
|
|
280
|
+
findOne! "INSERT INTO #{tableName} (#{cols}) VALUES (#{refs}) RETURNING *", vals
|
|
266
281
|
|
|
267
282
|
update: (id, data) ->
|
|
268
283
|
keys = Object.keys(data).filter (k) -> data[k] isnt undefined
|
|
@@ -271,19 +286,37 @@ export Model = (table, database = null) ->
|
|
|
271
286
|
findOne! "UPDATE #{tableName} SET #{sets} WHERE id = $1 RETURNING *", [id, ...vals]
|
|
272
287
|
|
|
273
288
|
upsert: (data, opts = {}) ->
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
sql
|
|
285
|
-
|
|
286
|
-
|
|
289
|
+
if Array.isArray(data)
|
|
290
|
+
return [] if data.length is 0
|
|
291
|
+
keys = Object.keys(data[0]).filter (k) -> data[0][k] isnt undefined
|
|
292
|
+
cols = keys.map((k) -> "\"#{k}\"").join(', ')
|
|
293
|
+
allVals = []
|
|
294
|
+
valueTuples = data.map (row, rowIdx) ->
|
|
295
|
+
refs = keys.map (k, colIdx) ->
|
|
296
|
+
allVals.push(row[k])
|
|
297
|
+
"$#{rowIdx * keys.length + colIdx + 1}"
|
|
298
|
+
"(#{refs.join(', ')})"
|
|
299
|
+
sql = "INSERT INTO #{tableName} (#{cols}) VALUES #{valueTuples.join(', ')}"
|
|
300
|
+
conflict = opts.on or opts.conflict
|
|
301
|
+
if conflict
|
|
302
|
+
updateCols = keys.filter((k) -> k isnt conflict)
|
|
303
|
+
sets = updateCols.map((k) -> "\"#{k}\" = EXCLUDED.\"#{k}\"").join(', ')
|
|
304
|
+
sql += " ON CONFLICT (\"#{conflict}\") DO UPDATE SET #{sets}"
|
|
305
|
+
sql += " RETURNING *"
|
|
306
|
+
findAll! sql, allVals
|
|
307
|
+
else
|
|
308
|
+
keys = Object.keys(data).filter (k) -> data[k] isnt undefined
|
|
309
|
+
cols = keys.map((k) -> "\"#{k}\"").join(', ')
|
|
310
|
+
refs = keys.map((_, i) -> "$#{i + 1}").join(', ')
|
|
311
|
+
vals = keys.map (k) -> data[k]
|
|
312
|
+
conflict = opts.on or opts.conflict
|
|
313
|
+
sql = "INSERT INTO #{tableName} (#{cols}) VALUES (#{refs})"
|
|
314
|
+
if conflict
|
|
315
|
+
updateCols = keys.filter((k) -> k isnt conflict)
|
|
316
|
+
sets = updateCols.map((k) -> "\"#{k}\" = EXCLUDED.\"#{k}\"").join(', ')
|
|
317
|
+
sql += " ON CONFLICT (\"#{conflict}\") DO UPDATE SET #{sets}"
|
|
318
|
+
sql += " RETURNING *"
|
|
319
|
+
findOne! sql, vals
|
|
287
320
|
|
|
288
321
|
destroy: (id) ->
|
|
289
322
|
findOne! "DELETE FROM #{tableName} WHERE id = $1 RETURNING *", [id]
|
package/db.rip
CHANGED
|
@@ -149,20 +149,54 @@ executeSQL = (sql, params = []) ->
|
|
|
149
149
|
catch err
|
|
150
150
|
{ error: err.message }
|
|
151
151
|
|
|
152
|
+
# Bulk insert via Appender API (fastest path for pure inserts)
|
|
153
|
+
executeAppend = (table, columns, rows) ->
|
|
154
|
+
console.log "APPEND #{table} (#{rows.length} rows)"
|
|
155
|
+
startTime = Date.now()
|
|
156
|
+
try
|
|
157
|
+
await conn.append(table, columns, rows)
|
|
158
|
+
{ rows: rows.length, time: (Date.now() - startTime) / 1000 }
|
|
159
|
+
catch err
|
|
160
|
+
{ error: err.message }
|
|
161
|
+
|
|
162
|
+
# Batch execute a prepared statement with multiple param sets
|
|
163
|
+
executeBatch = (sql, paramSets) ->
|
|
164
|
+
logSQL sql
|
|
165
|
+
startTime = Date.now()
|
|
166
|
+
try
|
|
167
|
+
await conn.queryBatch(sql, paramSets)
|
|
168
|
+
{ rows: paramSets.length, time: (Date.now() - startTime) / 1000 }
|
|
169
|
+
catch err
|
|
170
|
+
{ error: err.message }
|
|
171
|
+
|
|
152
172
|
# ==============================================================================
|
|
153
173
|
# JSON Endpoints (for custom apps)
|
|
154
174
|
# ==============================================================================
|
|
155
175
|
|
|
156
|
-
# POST /sql — Execute SQL
|
|
176
|
+
# POST /sql — Execute SQL, bulk append, or batch prepared statements
|
|
177
|
+
#
|
|
178
|
+
# Shapes:
|
|
179
|
+
# { sql } → raw execution
|
|
180
|
+
# { sql, params: [...] } → prepared statement
|
|
181
|
+
# { sql, params: [[...], [...]] } → batch prepared (reuse stmt)
|
|
182
|
+
# { table, columns, rows } → appender API (fastest insert)
|
|
157
183
|
post '/sql' ->
|
|
158
184
|
contentType = @req.header('content-type') or ''
|
|
159
185
|
if contentType.includes('application/json')
|
|
160
186
|
body = read()
|
|
187
|
+
|
|
188
|
+
if body?.table and body?.rows
|
|
189
|
+
return executeAppend body.table, body.columns, body.rows
|
|
190
|
+
|
|
161
191
|
sql = body?.sql or body
|
|
162
192
|
params = body?.params or []
|
|
193
|
+
|
|
194
|
+
if params.length > 0 and Array.isArray(params[0])
|
|
195
|
+
return executeBatch sql, params
|
|
163
196
|
else
|
|
164
197
|
sql = read 'body', 'string'
|
|
165
198
|
params = []
|
|
199
|
+
|
|
166
200
|
return { error: 'Empty query' } unless sql
|
|
167
201
|
executeSQL sql, params
|
|
168
202
|
|
package/lib/duckdb.mjs
CHANGED
|
@@ -89,6 +89,21 @@ const lib = dlopen(libPath, {
|
|
|
89
89
|
duckdb_bind_double: { args: ['ptr', 'u64', 'f64'], returns: 'i32' },
|
|
90
90
|
duckdb_bind_varchar: { args: ['ptr', 'u64', 'ptr'], returns: 'i32' },
|
|
91
91
|
duckdb_execute_prepared: { args: ['ptr', 'ptr'], returns: 'i32' },
|
|
92
|
+
duckdb_clear_bindings: { args: ['ptr'], returns: 'i32' },
|
|
93
|
+
|
|
94
|
+
// Appender API
|
|
95
|
+
duckdb_appender_create: { args: ['ptr', 'ptr', 'ptr', 'ptr'], returns: 'i32' },
|
|
96
|
+
duckdb_appender_error: { args: ['ptr'], returns: 'ptr' },
|
|
97
|
+
duckdb_appender_flush: { args: ['ptr'], returns: 'i32' },
|
|
98
|
+
duckdb_appender_close: { args: ['ptr'], returns: 'i32' },
|
|
99
|
+
duckdb_appender_destroy: { args: ['ptr'], returns: 'i32' },
|
|
100
|
+
duckdb_appender_end_row: { args: ['ptr'], returns: 'i32' },
|
|
101
|
+
duckdb_append_bool: { args: ['ptr', 'bool'], returns: 'i32' },
|
|
102
|
+
duckdb_append_int32: { args: ['ptr', 'i32'], returns: 'i32' },
|
|
103
|
+
duckdb_append_int64: { args: ['ptr', 'i64'], returns: 'i32' },
|
|
104
|
+
duckdb_append_double: { args: ['ptr', 'f64'], returns: 'i32' },
|
|
105
|
+
duckdb_append_varchar: { args: ['ptr', 'ptr'], returns: 'i32' },
|
|
106
|
+
duckdb_append_null: { args: ['ptr'], returns: 'i32' },
|
|
92
107
|
|
|
93
108
|
// Result inspection
|
|
94
109
|
duckdb_column_count: { args: ['ptr'], returns: 'u64' },
|
|
@@ -358,30 +373,7 @@ class Connection {
|
|
|
358
373
|
const stmtHandle = readPtr(stmtPtr);
|
|
359
374
|
|
|
360
375
|
try {
|
|
361
|
-
|
|
362
|
-
const paramIdx = BigInt(i + 1);
|
|
363
|
-
const value = params[i];
|
|
364
|
-
|
|
365
|
-
if (value === null || value === undefined) {
|
|
366
|
-
lib.duckdb_bind_null(stmtHandle, paramIdx);
|
|
367
|
-
} else if (typeof value === 'boolean') {
|
|
368
|
-
lib.duckdb_bind_boolean(stmtHandle, paramIdx, value);
|
|
369
|
-
} else if (typeof value === 'number') {
|
|
370
|
-
if (Number.isInteger(value)) {
|
|
371
|
-
lib.duckdb_bind_int64(stmtHandle, paramIdx, BigInt(value));
|
|
372
|
-
} else {
|
|
373
|
-
lib.duckdb_bind_double(stmtHandle, paramIdx, value);
|
|
374
|
-
}
|
|
375
|
-
} else if (typeof value === 'bigint') {
|
|
376
|
-
lib.duckdb_bind_int64(stmtHandle, paramIdx, value);
|
|
377
|
-
} else if (value instanceof Date) {
|
|
378
|
-
const strBytes = toCString(value.toISOString());
|
|
379
|
-
lib.duckdb_bind_varchar(stmtHandle, paramIdx, ptr(strBytes));
|
|
380
|
-
} else {
|
|
381
|
-
const strBytes = toCString(String(value));
|
|
382
|
-
lib.duckdb_bind_varchar(stmtHandle, paramIdx, ptr(strBytes));
|
|
383
|
-
}
|
|
384
|
-
}
|
|
376
|
+
this.#bindParams(stmtHandle, params);
|
|
385
377
|
|
|
386
378
|
const resultPtr = new Uint8Array(64); // duckdb_result struct is ~48 bytes
|
|
387
379
|
lib.duckdb_execute_prepared(stmtHandle, ptr(resultPtr));
|
|
@@ -778,6 +770,156 @@ class Connection {
|
|
|
778
770
|
return 'UNKNOWN';
|
|
779
771
|
}
|
|
780
772
|
|
|
773
|
+
#bindParams(stmtHandle, params) {
|
|
774
|
+
for (let i = 0; i < params.length; i++) {
|
|
775
|
+
const paramIdx = BigInt(i + 1);
|
|
776
|
+
const value = params[i];
|
|
777
|
+
|
|
778
|
+
if (value === null || value === undefined) {
|
|
779
|
+
lib.duckdb_bind_null(stmtHandle, paramIdx);
|
|
780
|
+
} else if (typeof value === 'boolean') {
|
|
781
|
+
lib.duckdb_bind_boolean(stmtHandle, paramIdx, value);
|
|
782
|
+
} else if (typeof value === 'number') {
|
|
783
|
+
if (Number.isInteger(value)) {
|
|
784
|
+
lib.duckdb_bind_int64(stmtHandle, paramIdx, BigInt(value));
|
|
785
|
+
} else {
|
|
786
|
+
lib.duckdb_bind_double(stmtHandle, paramIdx, value);
|
|
787
|
+
}
|
|
788
|
+
} else if (typeof value === 'bigint') {
|
|
789
|
+
lib.duckdb_bind_int64(stmtHandle, paramIdx, value);
|
|
790
|
+
} else if (value instanceof Date) {
|
|
791
|
+
const strBytes = toCString(value.toISOString());
|
|
792
|
+
lib.duckdb_bind_varchar(stmtHandle, paramIdx, ptr(strBytes));
|
|
793
|
+
} else {
|
|
794
|
+
const strBytes = toCString(String(value));
|
|
795
|
+
lib.duckdb_bind_varchar(stmtHandle, paramIdx, ptr(strBytes));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
#appendValue(appenderHandle, value) {
|
|
801
|
+
if (value === null || value === undefined) {
|
|
802
|
+
lib.duckdb_append_null(appenderHandle);
|
|
803
|
+
} else if (typeof value === 'boolean') {
|
|
804
|
+
lib.duckdb_append_bool(appenderHandle, value);
|
|
805
|
+
} else if (typeof value === 'number') {
|
|
806
|
+
if (Number.isInteger(value)) {
|
|
807
|
+
lib.duckdb_append_int64(appenderHandle, BigInt(value));
|
|
808
|
+
} else {
|
|
809
|
+
lib.duckdb_append_double(appenderHandle, value);
|
|
810
|
+
}
|
|
811
|
+
} else if (typeof value === 'bigint') {
|
|
812
|
+
lib.duckdb_append_int64(appenderHandle, value);
|
|
813
|
+
} else if (value instanceof Date) {
|
|
814
|
+
const strBytes = toCString(value.toISOString());
|
|
815
|
+
lib.duckdb_append_varchar(appenderHandle, ptr(strBytes));
|
|
816
|
+
} else {
|
|
817
|
+
const strBytes = toCString(String(value));
|
|
818
|
+
lib.duckdb_append_varchar(appenderHandle, ptr(strBytes));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Bulk insert rows using the DuckDB Appender API (fastest path)
|
|
824
|
+
* @param {string} table - Table name
|
|
825
|
+
* @param {string[]} columns - Column names
|
|
826
|
+
* @param {any[][]} rows - Array of value arrays (positional, matching columns)
|
|
827
|
+
* @returns {Promise<{rows: number}>}
|
|
828
|
+
*/
|
|
829
|
+
append(table, columns, rows) {
|
|
830
|
+
return withLock(() => {
|
|
831
|
+
const appenderPtr = allocPtr();
|
|
832
|
+
const tableBytes = toCString(table);
|
|
833
|
+
|
|
834
|
+
const status = lib.duckdb_appender_create(this.#handle, null, ptr(tableBytes), ptr(appenderPtr));
|
|
835
|
+
if (status !== 0) {
|
|
836
|
+
const handle = readPtr(appenderPtr);
|
|
837
|
+
if (handle) {
|
|
838
|
+
const errPtr = lib.duckdb_appender_error(handle);
|
|
839
|
+
const errMsg = errPtr ? fromCString(errPtr) : 'Failed to create appender';
|
|
840
|
+
lib.duckdb_appender_destroy(ptr(appenderPtr));
|
|
841
|
+
throw new Error(errMsg);
|
|
842
|
+
}
|
|
843
|
+
throw new Error('Failed to create appender');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const appenderHandle = readPtr(appenderPtr);
|
|
847
|
+
|
|
848
|
+
try {
|
|
849
|
+
for (const row of rows) {
|
|
850
|
+
for (const value of row) {
|
|
851
|
+
this.#appendValue(appenderHandle, value);
|
|
852
|
+
}
|
|
853
|
+
lib.duckdb_appender_end_row(appenderHandle);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const flushStatus = lib.duckdb_appender_flush(appenderHandle);
|
|
857
|
+
if (flushStatus !== 0) {
|
|
858
|
+
const errPtr = lib.duckdb_appender_error(appenderHandle);
|
|
859
|
+
const errMsg = errPtr ? fromCString(errPtr) : 'Appender flush failed';
|
|
860
|
+
throw new Error(errMsg);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return { rows: rows.length };
|
|
864
|
+
} finally {
|
|
865
|
+
lib.duckdb_appender_destroy(ptr(appenderPtr));
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Execute a prepared statement multiple times with different param sets
|
|
872
|
+
* @param {string} sql - SQL with $1, $2, ... placeholders
|
|
873
|
+
* @param {any[][]} paramSets - Array of param arrays
|
|
874
|
+
* @returns {Promise<{rows: number}>}
|
|
875
|
+
*/
|
|
876
|
+
queryBatch(sql, paramSets) {
|
|
877
|
+
return withLock(() => {
|
|
878
|
+
const stmtPtr = allocPtr();
|
|
879
|
+
const sqlBytes = toCString(sql);
|
|
880
|
+
|
|
881
|
+
const prepStatus = lib.duckdb_prepare(this.#handle, ptr(sqlBytes), ptr(stmtPtr));
|
|
882
|
+
if (prepStatus !== 0) {
|
|
883
|
+
const stmtHandle = readPtr(stmtPtr);
|
|
884
|
+
if (stmtHandle) {
|
|
885
|
+
const errPtr = lib.duckdb_prepare_error(stmtHandle);
|
|
886
|
+
const errMsg = errPtr ? fromCString(errPtr) : 'Failed to prepare statement';
|
|
887
|
+
lib.duckdb_destroy_prepare(ptr(stmtPtr));
|
|
888
|
+
throw new Error(errMsg);
|
|
889
|
+
}
|
|
890
|
+
throw new Error('Failed to prepare statement');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const stmtHandle = readPtr(stmtPtr);
|
|
894
|
+
let totalRows = 0;
|
|
895
|
+
|
|
896
|
+
try {
|
|
897
|
+
for (const params of paramSets) {
|
|
898
|
+
this.#bindParams(stmtHandle, params);
|
|
899
|
+
|
|
900
|
+
const resultPtr = new Uint8Array(64);
|
|
901
|
+
lib.duckdb_execute_prepared(stmtHandle, ptr(resultPtr));
|
|
902
|
+
|
|
903
|
+
const rp = ptr(resultPtr);
|
|
904
|
+
const errorPtr = lib.duckdb_result_error(rp);
|
|
905
|
+
if (errorPtr) {
|
|
906
|
+
const error = fromCString(errorPtr);
|
|
907
|
+
lib.duckdb_destroy_result(rp);
|
|
908
|
+
throw new Error(error);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
lib.duckdb_destroy_result(rp);
|
|
912
|
+
lib.duckdb_clear_bindings(stmtHandle);
|
|
913
|
+
totalRows++;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return { rows: totalRows };
|
|
917
|
+
} finally {
|
|
918
|
+
lib.duckdb_destroy_prepare(ptr(stmtPtr));
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
781
923
|
close() {
|
|
782
924
|
if (this.#ptrBuf) {
|
|
783
925
|
lib.duckdb_disconnect(ptr(this.#ptrBuf));
|