|
1 | 1 | package server |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "strings" |
4 | 5 | "testing" |
5 | 6 | ) |
6 | 7 |
|
@@ -358,3 +359,256 @@ func TestNestedBeginDetection(t *testing.T) { |
358 | 359 | t.Errorf("txStatus should still be %c after nested BEGIN detection, got %c", txStatusTransaction, c.txStatus) |
359 | 360 | } |
360 | 361 | } |
| 362 | + |
| 363 | +func TestQueryReturnsResults(t *testing.T) { |
| 364 | + tests := []struct { |
| 365 | + name string |
| 366 | + query string |
| 367 | + expected bool |
| 368 | + }{ |
| 369 | + // SELECT queries |
| 370 | + { |
| 371 | + name: "simple SELECT", |
| 372 | + query: "SELECT * FROM users", |
| 373 | + expected: true, |
| 374 | + }, |
| 375 | + { |
| 376 | + name: "SELECT with comment", |
| 377 | + query: "/*Fivetran*/ SELECT * FROM users", |
| 378 | + expected: true, |
| 379 | + }, |
| 380 | + { |
| 381 | + name: "SELECT with block and line comment", |
| 382 | + query: "/* comment */ -- line\nSELECT 1", |
| 383 | + expected: true, |
| 384 | + }, |
| 385 | + // WITH/CTE queries |
| 386 | + { |
| 387 | + name: "WITH clause", |
| 388 | + query: "WITH cte AS (SELECT 1) SELECT * FROM cte", |
| 389 | + expected: true, |
| 390 | + }, |
| 391 | + { |
| 392 | + name: "WITH clause with comment", |
| 393 | + query: "/*Fivetran*/ WITH cte AS (SELECT 1) SELECT * FROM cte", |
| 394 | + expected: true, |
| 395 | + }, |
| 396 | + // VALUES |
| 397 | + { |
| 398 | + name: "VALUES", |
| 399 | + query: "VALUES (1, 2), (3, 4)", |
| 400 | + expected: true, |
| 401 | + }, |
| 402 | + // SHOW |
| 403 | + { |
| 404 | + name: "SHOW", |
| 405 | + query: "SHOW TABLES", |
| 406 | + expected: true, |
| 407 | + }, |
| 408 | + // TABLE |
| 409 | + { |
| 410 | + name: "TABLE command", |
| 411 | + query: "TABLE users", |
| 412 | + expected: true, |
| 413 | + }, |
| 414 | + // EXPLAIN |
| 415 | + { |
| 416 | + name: "EXPLAIN", |
| 417 | + query: "EXPLAIN SELECT * FROM users", |
| 418 | + expected: true, |
| 419 | + }, |
| 420 | + // DESCRIBE |
| 421 | + { |
| 422 | + name: "DESCRIBE", |
| 423 | + query: "DESCRIBE users", |
| 424 | + expected: true, |
| 425 | + }, |
| 426 | + // Non-result queries |
| 427 | + { |
| 428 | + name: "INSERT", |
| 429 | + query: "INSERT INTO users VALUES (1)", |
| 430 | + expected: false, |
| 431 | + }, |
| 432 | + { |
| 433 | + name: "UPDATE", |
| 434 | + query: "UPDATE users SET name = 'test'", |
| 435 | + expected: false, |
| 436 | + }, |
| 437 | + { |
| 438 | + name: "DELETE", |
| 439 | + query: "DELETE FROM users", |
| 440 | + expected: false, |
| 441 | + }, |
| 442 | + { |
| 443 | + name: "CREATE TABLE", |
| 444 | + query: "CREATE TABLE test (id INT)", |
| 445 | + expected: false, |
| 446 | + }, |
| 447 | + { |
| 448 | + name: "CREATE TABLE with comment", |
| 449 | + query: "/*Fivetran*/ CREATE TABLE test (id INT)", |
| 450 | + expected: false, |
| 451 | + }, |
| 452 | + { |
| 453 | + name: "DROP TABLE", |
| 454 | + query: "DROP TABLE users", |
| 455 | + expected: false, |
| 456 | + }, |
| 457 | + { |
| 458 | + name: "BEGIN", |
| 459 | + query: "BEGIN", |
| 460 | + expected: false, |
| 461 | + }, |
| 462 | + { |
| 463 | + name: "COMMIT", |
| 464 | + query: "COMMIT", |
| 465 | + expected: false, |
| 466 | + }, |
| 467 | + { |
| 468 | + name: "ROLLBACK", |
| 469 | + query: "ROLLBACK", |
| 470 | + expected: false, |
| 471 | + }, |
| 472 | + { |
| 473 | + name: "SET", |
| 474 | + query: "SET search_path = public", |
| 475 | + expected: false, |
| 476 | + }, |
| 477 | + } |
| 478 | + |
| 479 | + for _, tt := range tests { |
| 480 | + t.Run(tt.name, func(t *testing.T) { |
| 481 | + result := queryReturnsResults(tt.query) |
| 482 | + if result != tt.expected { |
| 483 | + t.Errorf("queryReturnsResults(%q) = %v, want %v", tt.query, result, tt.expected) |
| 484 | + } |
| 485 | + }) |
| 486 | + } |
| 487 | +} |
| 488 | + |
| 489 | +// TestDescribeExecuteConsistency verifies that handleDescribe and handleExecute |
| 490 | +// use consistent logic to determine if a query returns results. |
| 491 | +// |
| 492 | +// This test reproduces the original bug where: |
| 493 | +// - handleDescribe used: strings.HasPrefix(upperQuery, "SELECT") |
| 494 | +// - handleExecute used: getCommandType(upperQuery) which strips comments |
| 495 | +// |
| 496 | +// For a query like "/* comment */ SELECT * FROM table": |
| 497 | +// - Old handleDescribe: "/* COMMENT */..." doesn't start with "SELECT" → sends NoData |
| 498 | +// - Old handleExecute: getCommandType strips comments → returns "SELECT" → sends results |
| 499 | +// |
| 500 | +// This mismatch caused: "Received resultset tuples, but no field structure for them" |
| 501 | +func TestDescribeExecuteConsistency(t *testing.T) { |
| 502 | + c := &clientConn{} |
| 503 | + |
| 504 | + // Queries that caused the bug: result-returning queries with leading comments |
| 505 | + problematicQueries := []struct { |
| 506 | + name string |
| 507 | + query string |
| 508 | + }{ |
| 509 | + {"block comment before SELECT", "/* comment */ SELECT * FROM users"}, |
| 510 | + {"block comment before SELECT no space", "/*comment*/SELECT 1"}, |
| 511 | + {"block comment before WITH", "/* query */ WITH cte AS (SELECT 1) SELECT * FROM cte"}, |
| 512 | + {"line comment before SELECT", "-- comment\nSELECT * FROM users"}, |
| 513 | + {"multiple block comments", "/* first */ /* second */ SELECT 1"}, |
| 514 | + } |
| 515 | + |
| 516 | + for _, tt := range problematicQueries { |
| 517 | + t.Run(tt.name, func(t *testing.T) { |
| 518 | + // Simulate the OLD handleDescribe logic (the bug) |
| 519 | + upperQuery := strings.ToUpper(strings.TrimSpace(tt.query)) |
| 520 | + oldDescribeWouldReturnResults := strings.HasPrefix(upperQuery, "SELECT") |
| 521 | + |
| 522 | + // Simulate the OLD handleExecute logic |
| 523 | + cmdType := c.getCommandType(upperQuery) |
| 524 | + oldExecuteWouldReturnResults := cmdType == "SELECT" |
| 525 | + |
| 526 | + // The bug: Describe says no results, Execute says results |
| 527 | + if oldDescribeWouldReturnResults != oldExecuteWouldReturnResults { |
| 528 | + t.Logf("BUG REPRODUCED: Old logic inconsistent for %q", tt.query) |
| 529 | + t.Logf(" Old Describe (HasPrefix SELECT): %v", oldDescribeWouldReturnResults) |
| 530 | + t.Logf(" Old Execute (getCommandType): %v (cmdType=%s)", oldExecuteWouldReturnResults, cmdType) |
| 531 | + } |
| 532 | + |
| 533 | + // Verify the NEW logic is consistent |
| 534 | + newDescribeWouldReturnResults := queryReturnsResults(tt.query) |
| 535 | + newExecuteWouldReturnResults := queryReturnsResults(tt.query) |
| 536 | + |
| 537 | + if newDescribeWouldReturnResults != newExecuteWouldReturnResults { |
| 538 | + t.Errorf("NEW logic still inconsistent for %q: Describe=%v, Execute=%v", |
| 539 | + tt.query, newDescribeWouldReturnResults, newExecuteWouldReturnResults) |
| 540 | + } |
| 541 | + |
| 542 | + // Verify the query is correctly identified as returning results |
| 543 | + if !newDescribeWouldReturnResults { |
| 544 | + t.Errorf("queryReturnsResults(%q) = false, but this query DOES return results", tt.query) |
| 545 | + } |
| 546 | + |
| 547 | + // Verify the old logic would have caused the bug |
| 548 | + if oldDescribeWouldReturnResults == oldExecuteWouldReturnResults { |
| 549 | + t.Logf("Note: Old logic was consistent for %q (no bug for this case)", tt.query) |
| 550 | + } |
| 551 | + }) |
| 552 | + } |
| 553 | +} |
| 554 | + |
| 555 | +// TestRowDescriptionMismatchScenario simulates the exact sequence that caused |
| 556 | +// the "Received resultset tuples, but no field structure for them" error. |
| 557 | +func TestRowDescriptionMismatchScenario(t *testing.T) { |
| 558 | + // Query with leading comment - common pattern from ETL tools |
| 559 | + query := "/* etl query */ SELECT * FROM information_schema.tables" |
| 560 | + |
| 561 | + // Step 1: Client sends Parse (creates prepared statement) |
| 562 | + // Step 2: Client sends Describe('S') for statement |
| 563 | + // Server checks if query returns results |
| 564 | + |
| 565 | + // OLD BEHAVIOR (the bug): |
| 566 | + upperQuery := strings.ToUpper(strings.TrimSpace(query)) |
| 567 | + oldWouldSendRowDescription := strings.HasPrefix(upperQuery, "SELECT") |
| 568 | + // Result: false (query starts with "/* ETL QUERY */", not "SELECT") |
| 569 | + // Server would send: NoData |
| 570 | + |
| 571 | + if oldWouldSendRowDescription { |
| 572 | + t.Error("Expected old logic to NOT detect this as SELECT (reproducing the bug)") |
| 573 | + } |
| 574 | + |
| 575 | + // Step 3: Client sends Bind (creates portal) |
| 576 | + // Step 4: Client sends Execute |
| 577 | + // Server executes query and checks if it returns results |
| 578 | + |
| 579 | + // OLD BEHAVIOR in Execute used getCommandType which strips comments: |
| 580 | + c := &clientConn{} |
| 581 | + cmdType := c.getCommandType(upperQuery) |
| 582 | + oldExecuteWouldQuery := cmdType == "SELECT" |
| 583 | + // Result: true (getCommandType strips comments, sees "SELECT") |
| 584 | + // Server would send: RowDescription + DataRows |
| 585 | + |
| 586 | + if !oldExecuteWouldQuery { |
| 587 | + t.Error("Expected old Execute logic to detect this as SELECT") |
| 588 | + } |
| 589 | + |
| 590 | + // THE BUG: Describe sent NoData, but Execute sent RowDescription + DataRows |
| 591 | + // Client received DataRows without expecting them → error |
| 592 | + |
| 593 | + if !oldWouldSendRowDescription && oldExecuteWouldQuery { |
| 594 | + t.Log("BUG CONFIRMED: Describe sent NoData, Execute sent results") |
| 595 | + t.Log("This causes: 'Received resultset tuples, but no field structure for them'") |
| 596 | + } |
| 597 | + |
| 598 | + // NEW BEHAVIOR (the fix): |
| 599 | + newWouldSendRowDescription := queryReturnsResults(query) |
| 600 | + newExecuteWouldQuery := queryReturnsResults(query) |
| 601 | + |
| 602 | + // Both should now be true and consistent |
| 603 | + if !newWouldSendRowDescription { |
| 604 | + t.Error("Fixed Describe should detect this as returning results") |
| 605 | + } |
| 606 | + if !newExecuteWouldQuery { |
| 607 | + t.Error("Fixed Execute should detect this as returning results") |
| 608 | + } |
| 609 | + if newWouldSendRowDescription != newExecuteWouldQuery { |
| 610 | + t.Error("Fixed logic should be consistent between Describe and Execute") |
| 611 | + } |
| 612 | + |
| 613 | + t.Log("FIX VERIFIED: Both Describe and Execute now correctly identify result-returning queries") |
| 614 | +} |
0 commit comments