|
| 1 | +package duckdb |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "database/sql" |
| 6 | + "database/sql/driver" |
| 7 | + "errors" |
| 8 | + |
| 9 | + "github.com/duckdb/duckdb-go/v2/mapping" |
| 10 | +) |
| 11 | + |
| 12 | +// QueryChunksContext executes a query and passes each native result chunk to |
| 13 | +// consume. It exists to read results without the per-row allocations of |
| 14 | +// database/sql: values can be read straight out of DuckDB's vectors, e.g., via |
| 15 | +// DataChunk.GetVarcharView. |
| 16 | +// |
| 17 | +// The chunks arrive in result order, and consume observes one chunk at a time. |
| 18 | +// Returning an error from consume stops the iteration and returns that error |
| 19 | +// unchanged, so a sentinel error stops the iteration early. |
| 20 | +// consume is not called at all for a result without rows. |
| 21 | +// |
| 22 | +// The connection is occupied for the whole call. consume must therefore not |
| 23 | +// issue queries on sqlConn, which would deadlock. Note that DuckDB materializes |
| 24 | +// the result before the first chunk is fetched, so this API bounds the memory |
| 25 | +// that the caller allocates, not the memory that DuckDB does. |
| 26 | +// |
| 27 | +// The same *DataChunk is reused for every chunk, and its C-allocated memory is |
| 28 | +// released once consume returns. Neither the chunk nor anything aliasing it may |
| 29 | +// be retained beyond that. args bypass the argument conversion of database/sql |
| 30 | +// and are bound as-is. If query holds multiple statements, all but the last are |
| 31 | +// executed, and only the last one yields chunks. |
| 32 | +func QueryChunksContext( |
| 33 | + ctx context.Context, |
| 34 | + sqlConn *sql.Conn, |
| 35 | + query string, |
| 36 | + consume func(*DataChunk) error, |
| 37 | + args ...any, |
| 38 | +) error { |
| 39 | + if sqlConn == nil { |
| 40 | + return getError(errAPI, errNilConn) |
| 41 | + } |
| 42 | + if consume == nil { |
| 43 | + return getError(errAPI, errNilChunkConsumer) |
| 44 | + } |
| 45 | + |
| 46 | + values := make([]driver.Value, len(args)) |
| 47 | + for i, arg := range args { |
| 48 | + values[i] = arg |
| 49 | + } |
| 50 | + |
| 51 | + return sqlConn.Raw(func(driverConn any) error { |
| 52 | + conn, ok := driverConn.(*Conn) |
| 53 | + if !ok { |
| 54 | + return getError(errAPI, invalidConnError(driverConn)) |
| 55 | + } |
| 56 | + return conn.queryChunksContext(ctx, query, argsToNamedArgs(values), consume) |
| 57 | + }) |
| 58 | +} |
| 59 | + |
| 60 | +// queryChunksContext prepares query on conn, streams its chunks to consume, and |
| 61 | +// closes the statement before returning. |
| 62 | +func (conn *Conn) queryChunksContext( |
| 63 | + ctx context.Context, |
| 64 | + query string, |
| 65 | + args []driver.NamedValue, |
| 66 | + consume func(*DataChunk) error, |
| 67 | +) error { |
| 68 | + if conn.closed { |
| 69 | + return errClosedCon |
| 70 | + } |
| 71 | + |
| 72 | + cleanupCtx := conn.setContext(ctx) |
| 73 | + defer cleanupCtx() |
| 74 | + |
| 75 | + return runWithCtxInterrupt(ctx, conn.conn, func(wctx context.Context) (err error) { |
| 76 | + prepared, err := conn.prepareStmts(wctx, query) |
| 77 | + if err != nil { |
| 78 | + return err |
| 79 | + } |
| 80 | + |
| 81 | + defer func() { |
| 82 | + closeErr := prepared.Close() |
| 83 | + switch { |
| 84 | + case err != nil && closeErr != nil: |
| 85 | + err = errors.Join(err, closeErr) |
| 86 | + case closeErr != nil: |
| 87 | + err = closeErr |
| 88 | + } |
| 89 | + }() |
| 90 | + |
| 91 | + return consumePreparedChunks(wctx, prepared, args, consume) |
| 92 | + }) |
| 93 | +} |
| 94 | + |
| 95 | +// consumePreparedChunks executes prepared and passes every result chunk to |
| 96 | +// consume, one at a time. It owns all C-allocated memory involved: the result is |
| 97 | +// destroyed on return, and each fetched chunk is freed after consume observes it. |
| 98 | +func consumePreparedChunks( |
| 99 | + ctx context.Context, |
| 100 | + prepared *Stmt, |
| 101 | + args []driver.NamedValue, |
| 102 | + consume func(*DataChunk) error, |
| 103 | +) error { |
| 104 | + result, err := prepared.execute(ctx, args) |
| 105 | + if err != nil { |
| 106 | + return err |
| 107 | + } |
| 108 | + defer mapping.DestroyResult(result) |
| 109 | + |
| 110 | + columnCount := mapping.ColumnCount(result) |
| 111 | + columnNames := make([]string, columnCount) |
| 112 | + for i := range columnNames { |
| 113 | + columnNames[i] = mapping.ColumnName(result, mapping.IdxT(i)) |
| 114 | + } |
| 115 | + |
| 116 | + var chunk DataChunk |
| 117 | + chunk.columnNames = columnNames |
| 118 | + for { |
| 119 | + if err := ctx.Err(); err != nil { |
| 120 | + return err |
| 121 | + } |
| 122 | + nativeChunk := mapping.FetchChunk(*result) |
| 123 | + if nativeChunk.Ptr == nil { |
| 124 | + if errMsg := mapping.ResultError(result); errMsg != "" { |
| 125 | + if err := ctx.Err(); err != nil { |
| 126 | + return err |
| 127 | + } |
| 128 | + return getError(errAPI, getDuckDBError(errMsg)) |
| 129 | + } |
| 130 | + return ctx.Err() |
| 131 | + } |
| 132 | + if err := chunk.initFromDuckDataChunk(nativeChunk, false); err != nil { |
| 133 | + mapping.DestroyDataChunk(&nativeChunk) |
| 134 | + return getError(errAPI, err) |
| 135 | + } |
| 136 | + |
| 137 | + err := consumeResultChunk(&chunk, consume) |
| 138 | + if err != nil { |
| 139 | + return err |
| 140 | + } |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +// consumeResultChunk passes chunk to consume and frees its C-allocated memory. |
| 145 | +func consumeResultChunk(chunk *DataChunk, consume func(*DataChunk) error) error { |
| 146 | + defer chunk.close() |
| 147 | + return consume(chunk) |
| 148 | +} |
0 commit comments