|
4 | 4 | Compares pandas backend results with Python backend to ensure correctness. |
5 | 5 | """ |
6 | 6 |
|
7 | | -import tempfile |
8 | | -from pathlib import Path |
9 | 7 |
|
10 | 8 | import pytest |
11 | 9 |
|
@@ -290,3 +288,337 @@ def test_explain_format(self, sample_csv): |
290 | 288 | assert "Load DataFrame" in plan |
291 | 289 | assert "Filter" in plan |
292 | 290 | assert "Limit" in plan |
| 291 | + |
| 292 | + |
| 293 | +class TestPandasBackendJSON: |
| 294 | + """Test pandas backend with JSON files""" |
| 295 | + |
| 296 | + @pytest.fixture |
| 297 | + def simple_json(self, tmp_path): |
| 298 | + """Create simple JSON array file""" |
| 299 | + import json |
| 300 | + json_file = tmp_path / "data.json" |
| 301 | + data = [ |
| 302 | + {"id": 1, "name": "Alice", "age": 30, "city": "NYC"}, |
| 303 | + {"id": 2, "name": "Bob", "age": 25, "city": "LA"}, |
| 304 | + {"id": 3, "name": "Charlie", "age": 35, "city": "Chicago"}, |
| 305 | + {"id": 4, "name": "Diana", "age": 28, "city": "NYC"}, |
| 306 | + ] |
| 307 | + json_file.write_text(json.dumps(data, indent=2)) |
| 308 | + return json_file |
| 309 | + |
| 310 | + @pytest.fixture |
| 311 | + def nested_json(self, tmp_path): |
| 312 | + """Create nested JSON file with records key""" |
| 313 | + import json |
| 314 | + json_file = tmp_path / "api_response.json" |
| 315 | + data = { |
| 316 | + "status": "success", |
| 317 | + "data": { |
| 318 | + "users": [ |
| 319 | + {"id": 1, "name": "Alice", "score": 95}, |
| 320 | + {"id": 2, "name": "Bob", "score": 87}, |
| 321 | + {"id": 3, "name": "Charlie", "score": 92}, |
| 322 | + ] |
| 323 | + }, |
| 324 | + "meta": {"count": 3} |
| 325 | + } |
| 326 | + json_file.write_text(json.dumps(data, indent=2)) |
| 327 | + return json_file |
| 328 | + |
| 329 | + def test_json_select_all(self, simple_json): |
| 330 | + """Test SELECT * on JSON file""" |
| 331 | + result = query(str(simple_json)).sql( |
| 332 | + "SELECT * FROM data", backend="pandas" |
| 333 | + ).to_list() |
| 334 | + |
| 335 | + assert len(result) == 4 |
| 336 | + assert result[0]["name"] == "Alice" |
| 337 | + assert result[0]["age"] == 30 |
| 338 | + |
| 339 | + def test_json_select_columns(self, simple_json): |
| 340 | + """Test SELECT specific columns from JSON""" |
| 341 | + result = query(str(simple_json)).sql( |
| 342 | + "SELECT name, city FROM data", backend="pandas" |
| 343 | + ).to_list() |
| 344 | + |
| 345 | + assert len(result) == 4 |
| 346 | + assert list(result[0].keys()) == ["name", "city"] |
| 347 | + assert result[0]["name"] == "Alice" |
| 348 | + assert result[0]["city"] == "NYC" |
| 349 | + |
| 350 | + def test_json_where_filter(self, simple_json): |
| 351 | + """Test WHERE clause on JSON data""" |
| 352 | + result = query(str(simple_json)).sql( |
| 353 | + "SELECT * FROM data WHERE age > 28", backend="pandas" |
| 354 | + ).to_list() |
| 355 | + |
| 356 | + assert len(result) == 2 |
| 357 | + names = sorted([r["name"] for r in result]) |
| 358 | + assert names == ["Alice", "Charlie"] |
| 359 | + |
| 360 | + def test_json_where_string_filter(self, simple_json): |
| 361 | + """Test WHERE clause with string comparison""" |
| 362 | + result = query(str(simple_json)).sql( |
| 363 | + "SELECT name, age FROM data WHERE city = 'NYC'", backend="pandas" |
| 364 | + ).to_list() |
| 365 | + |
| 366 | + assert len(result) == 2 |
| 367 | + names = sorted([r["name"] for r in result]) |
| 368 | + assert names == ["Alice", "Diana"] |
| 369 | + |
| 370 | + def test_json_limit(self, simple_json): |
| 371 | + """Test LIMIT on JSON data""" |
| 372 | + result = query(str(simple_json)).sql( |
| 373 | + "SELECT * FROM data LIMIT 2", backend="pandas" |
| 374 | + ).to_list() |
| 375 | + |
| 376 | + assert len(result) == 2 |
| 377 | + |
| 378 | + def test_json_order_by(self, simple_json): |
| 379 | + """Test ORDER BY on JSON data""" |
| 380 | + result = query(str(simple_json)).sql( |
| 381 | + "SELECT name, age FROM data ORDER BY age ASC", backend="pandas" |
| 382 | + ).to_list() |
| 383 | + |
| 384 | + ages = [r["age"] for r in result] |
| 385 | + assert ages == [25, 28, 30, 35] |
| 386 | + |
| 387 | + def test_json_nested_with_fragment(self, nested_json): |
| 388 | + """Test JSON with nested path using fragment syntax""" |
| 389 | + result = query(f"{nested_json}#json:data.users").sql( |
| 390 | + "SELECT * FROM users", backend="pandas" |
| 391 | + ).to_list() |
| 392 | + |
| 393 | + assert len(result) == 3 |
| 394 | + assert result[0]["name"] == "Alice" |
| 395 | + assert result[0]["score"] == 95 |
| 396 | + |
| 397 | + def test_json_auto_backend(self, simple_json): |
| 398 | + """Test that auto backend works with JSON files""" |
| 399 | + result = query(str(simple_json)).sql( |
| 400 | + "SELECT * FROM data WHERE age > 25", backend="auto" |
| 401 | + ) |
| 402 | + |
| 403 | + # Verify pandas backend was selected |
| 404 | + assert result.use_pandas is True |
| 405 | + |
| 406 | + # Verify results are correct |
| 407 | + rows = result.to_list() |
| 408 | + assert len(rows) == 3 |
| 409 | + |
| 410 | + |
| 411 | +class TestPandasBackendJSONL: |
| 412 | + """Test pandas backend with JSONL files""" |
| 413 | + |
| 414 | + @pytest.fixture |
| 415 | + def logs_jsonl(self, tmp_path): |
| 416 | + """Create JSONL log file""" |
| 417 | + import json |
| 418 | + jsonl_file = tmp_path / "logs.jsonl" |
| 419 | + logs = [ |
| 420 | + {"timestamp": "2024-01-01T10:00:00", "level": "INFO", "message": "Server started"}, |
| 421 | + {"timestamp": "2024-01-01T10:05:00", "level": "ERROR", "message": "Connection failed"}, |
| 422 | + {"timestamp": "2024-01-01T10:10:00", "level": "INFO", "message": "Request processed"}, |
| 423 | + {"timestamp": "2024-01-01T10:15:00", "level": "ERROR", "message": "Timeout"}, |
| 424 | + {"timestamp": "2024-01-01T10:20:00", "level": "INFO", "message": "Server shutdown"}, |
| 425 | + ] |
| 426 | + jsonl_file.write_text("\n".join(json.dumps(log) for log in logs)) |
| 427 | + return jsonl_file |
| 428 | + |
| 429 | + def test_jsonl_select_all(self, logs_jsonl): |
| 430 | + """Test SELECT * on JSONL file""" |
| 431 | + result = query(str(logs_jsonl)).sql( |
| 432 | + "SELECT * FROM logs", backend="pandas" |
| 433 | + ).to_list() |
| 434 | + |
| 435 | + assert len(result) == 5 |
| 436 | + assert result[0]["level"] == "INFO" |
| 437 | + |
| 438 | + def test_jsonl_where_filter(self, logs_jsonl): |
| 439 | + """Test WHERE clause on JSONL data""" |
| 440 | + result = query(str(logs_jsonl)).sql( |
| 441 | + "SELECT * FROM logs WHERE level = 'ERROR'", backend="pandas" |
| 442 | + ).to_list() |
| 443 | + |
| 444 | + assert len(result) == 2 |
| 445 | + messages = [r["message"] for r in result] |
| 446 | + assert "Connection failed" in messages |
| 447 | + assert "Timeout" in messages |
| 448 | + |
| 449 | + def test_jsonl_select_columns(self, logs_jsonl): |
| 450 | + """Test SELECT specific columns from JSONL""" |
| 451 | + result = query(str(logs_jsonl)).sql( |
| 452 | + "SELECT level, message FROM logs", backend="pandas" |
| 453 | + ).to_list() |
| 454 | + |
| 455 | + assert len(result) == 5 |
| 456 | + assert list(result[0].keys()) == ["level", "message"] |
| 457 | + |
| 458 | + def test_jsonl_limit(self, logs_jsonl): |
| 459 | + """Test LIMIT on JSONL data""" |
| 460 | + result = query(str(logs_jsonl)).sql( |
| 461 | + "SELECT * FROM logs LIMIT 3", backend="pandas" |
| 462 | + ).to_list() |
| 463 | + |
| 464 | + assert len(result) == 3 |
| 465 | + |
| 466 | + |
| 467 | +class TestPandasBackendXML: |
| 468 | + """Test pandas backend with XML files""" |
| 469 | + |
| 470 | + @pytest.fixture |
| 471 | + def simple_xml(self, tmp_path): |
| 472 | + """Create simple XML file""" |
| 473 | + xml_file = tmp_path / "data.xml" |
| 474 | + xml_content = """<?xml version="1.0"?> |
| 475 | +<data> |
| 476 | + <record> |
| 477 | + <id>1</id> |
| 478 | + <name>Alice</name> |
| 479 | + <age>30</age> |
| 480 | + <city>NYC</city> |
| 481 | + </record> |
| 482 | + <record> |
| 483 | + <id>2</id> |
| 484 | + <name>Bob</name> |
| 485 | + <age>25</age> |
| 486 | + <city>LA</city> |
| 487 | + </record> |
| 488 | + <record> |
| 489 | + <id>3</id> |
| 490 | + <name>Charlie</name> |
| 491 | + <age>35</age> |
| 492 | + <city>Chicago</city> |
| 493 | + </record> |
| 494 | +</data>""" |
| 495 | + xml_file.write_text(xml_content) |
| 496 | + return xml_file |
| 497 | + |
| 498 | + @pytest.fixture |
| 499 | + def products_xml(self, tmp_path): |
| 500 | + """Create products XML file""" |
| 501 | + xml_file = tmp_path / "products.xml" |
| 502 | + xml_content = """<?xml version="1.0"?> |
| 503 | +<catalog> |
| 504 | + <product id="p1" status="available"> |
| 505 | + <name>Laptop</name> |
| 506 | + <price>1200</price> |
| 507 | + <stock>50</stock> |
| 508 | + </product> |
| 509 | + <product id="p2" status="available"> |
| 510 | + <name>Mouse</name> |
| 511 | + <price>25</price> |
| 512 | + <stock>200</stock> |
| 513 | + </product> |
| 514 | + <product id="p3" status="discontinued"> |
| 515 | + <name>Keyboard</name> |
| 516 | + <price>80</price> |
| 517 | + <stock>0</stock> |
| 518 | + </product> |
| 519 | +</catalog>""" |
| 520 | + xml_file.write_text(xml_content) |
| 521 | + return xml_file |
| 522 | + |
| 523 | + def test_xml_select_all(self, simple_xml): |
| 524 | + """Test SELECT * on XML file""" |
| 525 | + result = query(str(simple_xml)).sql( |
| 526 | + "SELECT * FROM data", backend="pandas" |
| 527 | + ).to_list() |
| 528 | + |
| 529 | + assert len(result) == 3 |
| 530 | + assert result[0]["name"] == "Alice" |
| 531 | + assert result[0]["age"] == 30 |
| 532 | + |
| 533 | + def test_xml_select_columns(self, simple_xml): |
| 534 | + """Test SELECT specific columns from XML""" |
| 535 | + result = query(str(simple_xml)).sql( |
| 536 | + "SELECT name, city FROM data", backend="pandas" |
| 537 | + ).to_list() |
| 538 | + |
| 539 | + assert len(result) == 3 |
| 540 | + assert list(result[0].keys()) == ["name", "city"] |
| 541 | + |
| 542 | + def test_xml_where_filter(self, simple_xml): |
| 543 | + """Test WHERE clause on XML data""" |
| 544 | + result = query(str(simple_xml)).sql( |
| 545 | + "SELECT * FROM data WHERE age > 28", backend="pandas" |
| 546 | + ).to_list() |
| 547 | + |
| 548 | + assert len(result) == 2 |
| 549 | + names = sorted([r["name"] for r in result]) |
| 550 | + assert names == ["Alice", "Charlie"] |
| 551 | + |
| 552 | + def test_xml_with_element_selection(self, simple_xml): |
| 553 | + """Test XML with explicit element selection""" |
| 554 | + result = query(f"{simple_xml}#xml:record").sql( |
| 555 | + "SELECT name, age FROM data", backend="pandas" |
| 556 | + ).to_list() |
| 557 | + |
| 558 | + assert len(result) == 3 |
| 559 | + assert result[0]["name"] == "Alice" |
| 560 | + |
| 561 | + def test_xml_with_attributes(self, products_xml): |
| 562 | + """Test XML with attributes (@ prefix)""" |
| 563 | + result = query(f"{products_xml}#xml:product").sql( |
| 564 | + "SELECT name, price, stock FROM data WHERE price > 50", backend="pandas" |
| 565 | + ).to_list() |
| 566 | + |
| 567 | + assert len(result) == 2 |
| 568 | + names = sorted([r["name"] for r in result]) |
| 569 | + assert names == ["Keyboard", "Laptop"] |
| 570 | + |
| 571 | + def test_xml_order_by(self, simple_xml): |
| 572 | + """Test ORDER BY on XML data""" |
| 573 | + result = query(str(simple_xml)).sql( |
| 574 | + "SELECT name, age FROM data ORDER BY age DESC", backend="pandas" |
| 575 | + ).to_list() |
| 576 | + |
| 577 | + ages = [r["age"] for r in result] |
| 578 | + assert ages == [35, 30, 25] |
| 579 | + |
| 580 | + |
| 581 | +class TestPandasBackendMultiFormat: |
| 582 | + """Test pandas backend with multiple file formats in one query""" |
| 583 | + |
| 584 | + @pytest.fixture |
| 585 | + def setup_multiformat(self, tmp_path): |
| 586 | + """Create multiple format files""" |
| 587 | + import json |
| 588 | + |
| 589 | + # CSV file |
| 590 | + csv_file = tmp_path / "users.csv" |
| 591 | + csv_file.write_text("id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com\n") |
| 592 | + |
| 593 | + # JSON file |
| 594 | + json_file = tmp_path / "orders.json" |
| 595 | + orders = [ |
| 596 | + {"order_id": 101, "user_id": 1, "amount": 250}, |
| 597 | + {"order_id": 102, "user_id": 2, "amount": 150}, |
| 598 | + {"order_id": 103, "user_id": 1, "amount": 300}, |
| 599 | + ] |
| 600 | + json_file.write_text(json.dumps(orders)) |
| 601 | + |
| 602 | + return csv_file, json_file |
| 603 | + |
| 604 | + @pytest.mark.skip(reason="Table aliases in JOINs need further investigation") |
| 605 | + def test_join_csv_and_json(self, setup_multiformat): |
| 606 | + """Test JOIN between CSV and JSON files with pandas backend""" |
| 607 | + csv_file, json_file = setup_multiformat |
| 608 | + |
| 609 | + result = query(str(csv_file)).sql( |
| 610 | + f"SELECT u.name, o.order_id, o.amount " |
| 611 | + f"FROM {csv_file} u " |
| 612 | + f"INNER JOIN {json_file} o ON u.id = o.user_id", |
| 613 | + backend="pandas" |
| 614 | + ).to_list() |
| 615 | + |
| 616 | + assert len(result) == 3 |
| 617 | + |
| 618 | + # Check Alice has 2 orders |
| 619 | + alice_orders = [r for r in result if r["name"] == "Alice"] |
| 620 | + assert len(alice_orders) == 2 |
| 621 | + |
| 622 | + # Check total amounts |
| 623 | + alice_total = sum(r["amount"] for r in alice_orders) |
| 624 | + assert alice_total == 550 |
0 commit comments