@rip-lang/db 1.3.2 → 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.
Files changed (4) hide show
  1. package/client.rip +51 -18
  2. package/db.rip +35 -1
  3. package/lib/duckdb.mjs +166 -24
  4. package/package.json +2 -2
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
- keys = Object.keys(data).filter (k) -> data[k] isnt undefined
262
- cols = keys.map((k) -> "\"#{k}\"").join(', ')
263
- refs = keys.map((_, i) -> "$#{i + 1}").join(', ')
264
- vals = keys.map (k) -> data[k]
265
- findOne! "INSERT INTO #{tableName} (#{cols}) VALUES (#{refs}) RETURNING *", vals
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
- keys = Object.keys(data).filter (k) -> data[k] isnt undefined
275
- cols = keys.map((k) -> "\"#{k}\"").join(', ')
276
- refs = keys.map((_, i) -> "$#{i + 1}").join(', ')
277
- vals = keys.map (k) -> data[k]
278
-
279
- conflict = opts.on or opts.conflict
280
- sql = "INSERT INTO #{tableName} (#{cols}) VALUES (#{refs})"
281
- if conflict
282
- updateCols = keys.filter((k) -> k isnt conflict)
283
- sets = updateCols.map((k) -> "\"#{k}\" = EXCLUDED.\"#{k}\"").join(', ')
284
- sql += " ON CONFLICT (\"#{conflict}\") DO UPDATE SET #{sets}"
285
- sql += " RETURNING *"
286
- findOne! sql, vals
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 with optional parameters
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
- for (let i = 0; i < params.length; i++) {
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/db",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "DuckDB server with official DuckDB UI — pure Bun FFI",
5
5
  "type": "module",
6
6
  "main": "db.rip",
@@ -39,7 +39,7 @@
39
39
  "license": "MIT",
40
40
  "dependencies": {
41
41
  "rip-lang": "^3.12.0",
42
- "@rip-lang/api": "^1.2.6"
42
+ "@rip-lang/api": "^1.2.7"
43
43
  },
44
44
  "files": [
45
45
  "db.rip",