setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); echo "✅ Database connection established\n\n"; // Drop test table if exists echo "Dropping test table if exists...\n"; $pdo->exec("DROP TABLE IF EXISTS test_jsonb_table"); echo "✅ Cleanup complete\n\n"; // Create test table with JSONB column echo "Creating test table with JSONB column...\n"; $pdo->exec(" CREATE TABLE test_jsonb_table ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, metadata JSONB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) "); echo "✅ Table created successfully\n\n"; // Create GIN index on JSONB column echo "Creating GIN index on JSONB column...\n"; $pdo->exec("CREATE INDEX idx_metadata_gin ON test_jsonb_table USING GIN (metadata)"); echo "✅ GIN index created successfully\n\n"; // Verify index was created with correct method echo "Verifying index method...\n"; $stmt = $pdo->query(" SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'test_jsonb_table' AND indexname = 'idx_metadata_gin' "); $indexInfo = $stmt->fetch(PDO::FETCH_ASSOC); if ($indexInfo && str_contains($indexInfo['indexdef'], 'USING gin')) { echo "✅ Index confirmed to use GIN method\n"; echo " Definition: {$indexInfo['indexdef']}\n\n"; } else { echo "❌ Index not found or not using GIN method\n\n"; exit(1); } // Insert test data with JSONB echo "Inserting test data...\n"; $testData = [ [ 'name' => 'Product A', 'metadata' => json_encode([ 'category' => 'electronics', 'price' => 299.99, 'tags' => ['new', 'sale', 'featured'], 'specs' => [ 'cpu' => 'Intel i7', 'ram' => '16GB', 'storage' => '512GB SSD' ] ]) ], [ 'name' => 'Product B', 'metadata' => json_encode([ 'category' => 'electronics', 'price' => 499.99, 'tags' => ['premium', 'featured'], 'specs' => [ 'cpu' => 'AMD Ryzen 9', 'ram' => '32GB', 'storage' => '1TB SSD' ] ]) ], [ 'name' => 'Product C', 'metadata' => json_encode([ 'category' => 'books', 'price' => 19.99, 'tags' => ['bestseller'], 'author' => 'John Doe', 'isbn' => '978-3-16-148410-0' ]) ] ]; $insertStmt = $pdo->prepare(" INSERT INTO test_jsonb_table (name, metadata) VALUES (:name, :metadata) "); foreach ($testData as $data) { $insertStmt->execute([ 'name' => $data['name'], 'metadata' => $data['metadata'] ]); } echo "✅ Test data inserted (3 products)\n\n"; // Test JSONB queries with different operators echo "Testing JSONB queries:\n"; echo "=====================\n\n"; // Test 1: Contains operator (@>) echo "1. Query: Find all electronics\n"; $stmt = $pdo->query(" SELECT name, metadata->>'category' as category, metadata->>'price' as price FROM test_jsonb_table WHERE metadata @> '{\"category\": \"electronics\"}' "); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); echo " Found " . count($results) . " products:\n"; foreach ($results as $row) { echo " - {$row['name']}: {$row['category']} (\${$row['price']})\n"; } echo "\n"; // Test 2: JSON path query (using jsonb_exists to avoid PDO parameter conflict with ?) echo "2. Query: Find products with 'featured' tag\n"; $stmt = $pdo->query(" SELECT name, metadata->'tags' as tags FROM test_jsonb_table WHERE jsonb_exists(metadata->'tags', 'featured') "); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); echo " Found " . count($results) . " products:\n"; foreach ($results as $row) { echo " - {$row['name']}: " . $row['tags'] . "\n"; } echo "\n"; // Test 3: Nested JSON query echo "3. Query: Find products with specific CPU\n"; $stmt = $pdo->query(" SELECT name, metadata->'specs'->>'cpu' as cpu, metadata->'specs'->>'ram' as ram FROM test_jsonb_table WHERE metadata->'specs'->>'cpu' LIKE '%Intel%' "); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); echo " Found " . count($results) . " products:\n"; foreach ($results as $row) { echo " - {$row['name']}: {$row['cpu']}, {$row['ram']} RAM\n"; } echo "\n"; // Test 4: Query by price range echo "4. Query: Find products under $100\n"; $stmt = $pdo->query(" SELECT name, (metadata->>'price')::numeric as price FROM test_jsonb_table WHERE (metadata->>'price')::numeric < 100 "); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); echo " Found " . count($results) . " products:\n"; foreach ($results as $row) { echo " - {$row['name']}: \${$row['price']}\n"; } echo "\n"; // Test 5: Existence check (using jsonb_exists to avoid PDO parameter conflict) echo "5. Query: Find products with author field\n"; $stmt = $pdo->query(" SELECT name, metadata->>'author' as author FROM test_jsonb_table WHERE jsonb_exists(metadata, 'author') "); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); echo " Found " . count($results) . " products:\n"; foreach ($results as $row) { echo " - {$row['name']}: by {$row['author']}\n"; } echo "\n"; // Verify GIN index is being used via EXPLAIN echo "Verifying GIN index usage with EXPLAIN:\n"; $stmt = $pdo->query(" EXPLAIN (FORMAT JSON) SELECT name FROM test_jsonb_table WHERE metadata @> '{\"category\": \"electronics\"}' "); $explainResult = $stmt->fetch(PDO::FETCH_ASSOC); $explainJson = json_decode($explainResult['QUERY PLAN'], true); $usesGinIndex = false; $planStr = json_encode($explainJson, JSON_PRETTY_PRINT); if (str_contains($planStr, 'idx_metadata_gin') || str_contains($planStr, 'Bitmap Index Scan')) { echo "✅ Query optimizer is using GIN index\n"; $usesGinIndex = true; } else { echo "⚠️ GIN index may not be used (table might be too small for index to be beneficial)\n"; } echo "\n"; // Cleanup echo "Cleaning up...\n"; $pdo->exec("DROP TABLE IF EXISTS test_jsonb_table"); echo "✅ Test table dropped\n"; echo "\n✅ All JSONB with GIN Index tests passed!\n"; echo "\nSummary:\n"; echo "========\n"; echo "✅ JSONB column type supported\n"; echo "✅ GIN index created successfully\n"; echo "✅ Contains operator (@>) works\n"; echo "✅ Existence check (jsonb_exists) works\n"; echo "✅ JSON path queries work\n"; echo "✅ Nested JSON queries work\n"; echo "✅ Type casting works\n"; if ($usesGinIndex) { echo "✅ Query optimizer uses GIN index\n"; } echo "\nNote: Using jsonb_exists() instead of ? operator due to PDO parameter placeholder conflict\n"; } catch (\Exception $e) { echo "❌ Error: " . $e->getMessage() . "\n"; echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; exit(1); }