Hybrid data sources (JSON + database)

Hybrid sources represent one of ObjectQuel's unique capabilities - the ability to query JSON files as if they were database tables. This feature allows you to:

  • Unify Data Sources: Combine database entities with external JSON data in single queries
  • Flexible Integration: Work with APIs, configuration files, or static data without importing into your database
  • Rapid Prototyping: Test with mock data before implementing full database schemas
  • External System Integration: Query third-party data sources using familiar ObjectQuel syntax
  • Hybrid Relationships: Create relationships between database entities and JSON data

JSON sources support ObjectQuel query operations including filtering and sorting:

// Filter by properties
$results = $entityManager->executeQuery("
    range of product is json_source('catalog/products.json')
    retrieve (product) where product.category = :category
    sort by product.price asc
", ['category' => 'electronics']);

// Complex filtering with multiple conditions
$results = $entityManager->executeQuery("
    range of item is json_source('inventory/items.json')
    retrieve (item) where item.price > :minPrice
    and item.inStock = true
    sort by item.name
", ['minPrice' => 25.00]);

// Property selection from JSON
$results = $entityManager->executeQuery("
    range of product is json_source('data/products.json')
    retrieve (product.name, product.price) where product.id = :productId
", ['productId' => 15]);

JSON File Structure

JSON files should contain an array of objects with consistent structure:

[
  {
    "id": 1,
    "name": "Wireless Headphones",
    "price": 99.99,
    "category": "electronics",
    "inStock": true,
    "tags": ["wireless", "audio", "bluetooth"]
  },
  {
    "id": 2,
    "name": "Gaming Mouse",
    "price": 59.99,
    "category": "electronics",
    "inStock": false,
    "tags": ["gaming", "precision", "rgb"]
  },
  {
    "id": 3,
    "name": "Coffee Mug",
    "price": 12.99,
    "category": "kitchen",
    "inStock": true,
    "tags": ["ceramic", "dishwasher-safe"]
  }
]

JSONPath Prefiltering

ObjectQuel uses the softcreatr/jsonpath library to provide powerful JSONPath prefiltering capabilities. The second parameter of json_source() accepts a JSONPath expression that filters the JSON data before it reaches the ObjectQuel query processor:

// Filter to only electronics products before querying
$results = $entityManager->executeQuery("
    range of product is json_source('catalog/products.json', '$.products[?(@.category == \"electronics\")]')
    retrieve (product) where product.price > :minPrice
", ['minPrice' => 50.00]);

// Extract nested data structures
$results = $entityManager->executeQuery("
    range of order is json_source('orders/daily.json', '$.orders[*].items[*]')
    retrieve (order) where order.quantity > :minQty
", ['minQty' => 2]);

// Filter by array conditions
$results = $entityManager->executeQuery("
    range of user is json_source('users/profiles.json', '$.users[?(@.roles[*] == \"admin\")]')
    retrieve (user) where user.active = true
");

JSONPath Expression Examples

Common JSONPath patterns for prefiltering JSON data:

JSONPath Expression Description Example Usage
$.products[*] All products in a products array Extract products from nested structure
$[?(@.status == "active")] Filter by status field Only active records
$.data.items[?(@.price > 100)] Nested filtering by price Expensive items from nested data
$[?(@.tags[*] == "featured")] Filter by array contents Items with "featured" tag
$.results[0:10] Slice first 10 results Pagination at JSON level
$..items[?(@.inStock)] Recursive search with filter In-stock items anywhere in structure

Complex JSON Structure Handling

JSONPath prefiltering is particularly useful for complex, nested JSON structures:

{
  "metadata": {
    "version": "1.0",
    "timestamp": "2025-06-04T10:30:00Z"
  },
  "data": {
    "products": [
      {
        "id": 1,
        "name": "Laptop",
        "category": "electronics",
        "specifications": {
          "cpu": "Intel i7",
          "ram": "16GB"
        },
        "availability": {
          "inStock": true,
          "quantity": 5
        }
      }
    ],
    "categories": [
      {
        "id": "electronics",
        "name": "Electronics",
        "active": true
      }
    ]
  }
}
// Extract only products with sufficient stock
$results = $entityManager->executeQuery("
    range of product is json_source('complex_catalog.json', '$.data.products[?(@.availability.quantity > 0)]')
    retrieve (product) where product.category = :category
", ['category' => 'electronics']);

// Get active categories only
$results = $entityManager->executeQuery("
    range of cat is json_source('complex_catalog.json', '$.data.categories[?(@.active == true)]')
    retrieve (cat) where cat.name = :name
", ['name' => 'Electronics']);

Performance Benefits of Prefiltering

JSONPath prefiltering provides significant performance advantages:

  • Reduced Memory Usage: Only relevant data is loaded into ObjectQuel's query processor
  • Faster Query Processing: Smaller datasets result in faster WHERE clause evaluation
  • Selective Loading: Extract specific parts of complex nested structures
// Without prefiltering - loads entire 10MB file
$results = $entityManager->executeQuery("
    range of log is json_source('logs/massive_log.json')
    retrieve (log) where log.level = 'ERROR'
");

// With prefiltering - only loads error entries
$results = $entityManager->executeQuery("
    range of log is json_source('logs/massive_log.json', '$[?(@.level == \"ERROR\")]')
    retrieve (log) where log.timestamp > :since
", ['since' => '2025-06-01']);

Combining Database and JSON Sources

The real power of hybrid sources becomes apparent when combining database entities with JSON data. Currently, JSON sources are always left joined with database entities. Support for cross and inner joins will be added in future releases and will be automatically inferred from the query context:

// Join database orders with JSON product catalog
$results = $entityManager->executeQuery("
    range of order is App\\Entity\\OrderEntity
    range of product is json_source('external/product_catalog.json')
    retrieve (order, product) where order.productId = product.id
    and order.status = :status
    sort by order.orderDate desc
", ['status' => 'pending']);

// Cross-reference customer data with JSON preferences
$results = $entityManager->executeQuery("
    range of customer is App\\Entity\\CustomerEntity
    range of pref is json_source('preferences/user_preferences.json')
    retrieve (customer, pref) where customer.customerId = pref.userId
    and pref.notifications = true
");

JSON Source Relationships

You can establish relationships between database entities and JSON sources using WHERE clauses to link the data:

// Link orders to external product information
$results = $entityManager->executeQuery("
    range of order is App\\Entity\\OrderEntity
    range of product is json_source('api_cache/products.json')
    retrieve (order, product.name, product.manufacturer)
    where order.productSku = product.sku
    and order.customerId = :customerId
", ['customerId' => 12345]);

// Multi-level relationships with JSON
$results = $entityManager->executeQuery("
    range of user is App\\Entity\\UserEntity
    range of profile is json_source('profiles/user_profiles.json')
    range of settings is json_source('settings/app_settings.json')
    retrieve (user, profile.displayName, settings.colorScheme)
    where user.userId = profile.user_id
    and profile.theme = settings.theme_name
    and user.active = true
");

Note: The via syntax for establishing relationships with JSON sources is not currently supported but may be added in future releases.

Advanced JSON Operations

JSON sources support advanced ObjectQuel features including pattern matching:

// Pattern matching in JSON
$results = $entityManager->executeQuery("
    range of log is json_source('logs/access_logs.json')
    retrieve (log) where log.userAgent = 'Mozilla*'
    and log.responseCode = 200
");

// Array field querying
$results = $entityManager->executeQuery("
    range of product is json_source('data/products.json')
    retrieve (product) where 'wireless' in product.tags
");

JSON Source Performance Considerations

When working with JSON sources, consider these performance optimization strategies:

  • File Size: Keep JSON files reasonably sized (under 10MB) for optimal performance
  • Indexing: For large JSON datasets, consider importing into database tables with proper indexes
  • Memory Usage: Large JSON files are loaded entirely into memory during query processing

Error Handling

When working with JSON sources, implement proper error handling for common scenarios:

try {
    $results = $entityManager->executeQuery("
        range of data is json_source('external/api_data.json')
        retrieve (data) where data.status = 'active'
    ");
} catch (QuelException $e) {
    // Handle file not found, invalid JSON, or query errors
    if (str_contains($e->getMessage(), 'json_source')) {
        // Fallback to default data or log error
        error_log("JSON source error: " . $e->getMessage());
        $results = []; // or provide default data
    } else {
        throw $e; // Re-throw other query errors
    }
}

Best Practices

Follow these guidelines when implementing hybrid sources:

  • Consistent Schema: Maintain consistent JSON object structures within each file
  • File Organization: Use logical directory structures for JSON data organization
  • Validation: Validate JSON file integrity before querying
  • Documentation: Document JSON schema expectations for team members
  • Backup Strategy: Include JSON data files in your backup and version control systems
  • Security: Validate file paths to prevent directory traversal attacks

Hybrid sources provide unprecedented flexibility in data integration, enabling ObjectQuel applications to seamlessly work with diverse data sources while maintaining a consistent, powerful query interface.