Skip to content

Commit 17a64ea

Browse files
fuziontechclaude
andcommitted
fix: change query_log normalized_query_hash column from UBIGINT to BIGINT
The previous fix (#366) cast uint64 hashes to int64 to work around Go's database/sql driver rejecting high-bit uint64 values. However, the column was still UBIGINT, so DuckDB rejected negative int64 values with "Type INT64 with value ... can't be cast because the value is out of range for the destination type UINT64". Change the column to BIGINT and add a best-effort ALTER TABLE migration for existing tables. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 153607b commit 17a64ea

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎server/querylog.go‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func NewQueryLogger(cfg Config) (*QueryLogger, error) {
114114
query VARCHAR NOT NULL,
115115
transpiled_query VARCHAR,
116116
query_kind VARCHAR,
117-
normalized_query_hash UBIGINT,
117+
normalized_query_hash BIGINT,
118118
result_rows BIGINT,
119119
written_rows BIGINT,
120120
exception_code VARCHAR,
@@ -134,6 +134,16 @@ func NewQueryLogger(cfg Config) (*QueryLogger, error) {
134134
return nil, fmt.Errorf("querylog: create table: %w", err)
135135
}
136136

137+
// Migrate normalized_query_hash from UBIGINT to BIGINT for existing tables.
138+
// The Go database/sql driver cannot bind uint64 values with the high bit set,
139+
// so we cast to int64 before inserting. A BIGINT column accepts negative int64
140+
// values without a conversion error.
141+
if _, err := db.Exec("ALTER TABLE ducklake.system.query_log ALTER COLUMN normalized_query_hash SET DATA TYPE BIGINT"); err != nil {
142+
// Ignore error — column may already be BIGINT (new table) or DuckLake
143+
// may not support ALTER TYPE. Either way, this is best-effort.
144+
slog.Debug("querylog: ALTER normalized_query_hash to BIGINT (may already be correct).", "error", err)
145+
}
146+
137147
// Configure data inlining
138148
inlineStmt := fmt.Sprintf(
139149
"CALL ducklake.set_option('data_inlining_row_limit', %d, schema => 'system', table_name => 'query_log')",

‎server/querylog_test.go‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,59 @@ func TestQueryLoggerStopIsIdempotent(t *testing.T) {
180180
ql.Stop()
181181
}
182182

183+
func TestHighBitHashInsertIntoBigint(t *testing.T) {
184+
db, err := sql.Open("duckdb", ":memory:")
185+
if err != nil {
186+
t.Fatalf("open duckdb: %v", err)
187+
}
188+
defer db.Close()
189+
190+
_, err = db.Exec("CREATE TABLE test_hash (h BIGINT)")
191+
if err != nil {
192+
t.Fatalf("create table: %v", err)
193+
}
194+
195+
// Use a hash with the high bit set — this is the scenario that caused the
196+
// "INT64 value can't be cast to UINT64" error when the column was UBIGINT.
197+
var highBitHash uint64 = 0xD700_0000_0000_0000
198+
199+
// This is the same cast used in flushBatch: int64(uint64_value)
200+
_, err = db.Exec("INSERT INTO test_hash VALUES ($1)", int64(highBitHash))
201+
if err != nil {
202+
t.Fatalf("insert failed (this was the original bug): %v", err)
203+
}
204+
205+
var stored int64
206+
err = db.QueryRow("SELECT h FROM test_hash").Scan(&stored)
207+
if err != nil {
208+
t.Fatalf("query failed: %v", err)
209+
}
210+
if uint64(stored) != highBitHash {
211+
t.Errorf("hash round-trip mismatch: got uint64(%d) = %d, want %d", stored, uint64(stored), highBitHash)
212+
}
213+
}
214+
215+
// TestHighBitHashRejectsUbigint confirms that UBIGINT rejects negative int64
216+
// values — this is the bug we fixed by switching to BIGINT.
217+
func TestHighBitHashRejectsUbigint(t *testing.T) {
218+
db, err := sql.Open("duckdb", ":memory:")
219+
if err != nil {
220+
t.Fatalf("open duckdb: %v", err)
221+
}
222+
defer db.Close()
223+
224+
_, err = db.Exec("CREATE TABLE test_hash_u (h UBIGINT)")
225+
if err != nil {
226+
t.Fatalf("create table: %v", err)
227+
}
228+
229+
var highBitHash uint64 = 0xD700_0000_0000_0000
230+
_, err = db.Exec("INSERT INTO test_hash_u VALUES ($1)", int64(highBitHash))
231+
if err == nil {
232+
t.Fatal("expected error inserting negative int64 into UBIGINT, but insert succeeded")
233+
}
234+
}
235+
183236
func TestSplitHostPort(t *testing.T) {
184237
host, port, err := splitHostPort("192.168.1.1:5432")
185238
if err != nil {

0 commit comments

Comments
 (0)