or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

database-management.mdevents-utilities.mdindex.mdkey-ranges.mdobject-stores.mdtransactions-cursors.md
tile.json

key-ranges.mddocs/

Key Ranges and Queries

Advanced querying capabilities with key ranges, bounds, and filtering for precise data selection.

Capabilities

IDBKeyRange Static Methods

Factory methods for creating key ranges to filter queries and cursor iterations.

interface IDBKeyRangeStatic {
  /**
   * Creates a range containing only the specified key
   * @param value - The single key value to match
   * @returns Key range matching exactly the given value
   */
  only(value: IDBValidKey): IDBKeyRange;
  
  /**
   * Creates a range with a lower bound
   * @param lower - The lower bound key
   * @param open - Whether lower bound is exclusive (default: false)
   * @returns Key range from lower bound to end
   */
  lowerBound(lower: IDBValidKey, open?: boolean): IDBKeyRange;
  
  /**
   * Creates a range with an upper bound
   * @param upper - The upper bound key
   * @param open - Whether upper bound is exclusive (default: false)
   * @returns Key range from start to upper bound
   */
  upperBound(upper: IDBValidKey, open?: boolean): IDBKeyRange;
  
  /**
   * Creates a range with both lower and upper bounds
   * @param lower - The lower bound key
   * @param upper - The upper bound key
   * @param lowerOpen - Whether lower bound is exclusive (default: false)
   * @param upperOpen - Whether upper bound is exclusive (default: false)
   * @returns Key range between the bounds
   */
  bound(
    lower: IDBValidKey,
    upper: IDBValidKey,
    lowerOpen?: boolean,
    upperOpen?: boolean
  ): IDBKeyRange;
}

IDBKeyRange Instance

Represents a range of keys for querying and filtering operations.

interface IDBKeyRange {
  /** Lower bound value (readonly) */
  readonly lower: IDBValidKey | undefined;
  
  /** Upper bound value (readonly) */
  readonly upper: IDBValidKey | undefined;
  
  /** Whether lower bound is exclusive (readonly) */
  readonly lowerOpen: boolean;
  
  /** Whether upper bound is exclusive (readonly) */
  readonly upperOpen: boolean;
  
  /**
   * Tests whether a key is included in this range
   * @param key - Key to test
   * @returns True if key is within the range
   */
  includes(key: IDBValidKey): boolean;
}

Usage Examples:

import "fake-indexeddb/auto";

// Single value range
const exactRange = IDBKeyRange.only("electronics");
store.index("by_category").getAll(exactRange);

// Lower bound range (>= 100)
const minPriceRange = IDBKeyRange.lowerBound(100);
store.index("by_price").getAll(minPriceRange);

// Lower bound range (> 100, exclusive)
const aboveMinRange = IDBKeyRange.lowerBound(100, true);
store.index("by_price").getAll(aboveMinRange);

// Upper bound range (<= 500)
const maxPriceRange = IDBKeyRange.upperBound(500);
store.index("by_price").getAll(maxPriceRange);

// Upper bound range (< 500, exclusive)
const belowMaxRange = IDBKeyRange.upperBound(500, true);
store.index("by_price").getAll(belowMaxRange);

// Bounded range (100 <= price <= 500)
const priceRange = IDBKeyRange.bound(100, 500);
store.index("by_price").getAll(priceRange);

// Bounded range (100 < price < 500, both exclusive)
const exclusiveRange = IDBKeyRange.bound(100, 500, true, true);
store.index("by_price").getAll(exclusiveRange);

Advanced Key Range Patterns

Date Range Queries

// Query records within date range
const startDate = new Date("2024-01-01");
const endDate = new Date("2024-12-31");

const dateRange = IDBKeyRange.bound(startDate, endDate);

const tx = db.transaction("orders", "readonly");
const store = tx.objectStore("orders");
const dateIndex = store.index("by_date");

dateIndex.getAll(dateRange).onsuccess = (event) => {
  const orders = event.target.result;
  console.log(`Orders from 2024: ${orders.length}`);
};

// Last 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const recentRange = IDBKeyRange.lowerBound(thirtyDaysAgo);

dateIndex.getAll(recentRange).onsuccess = (event) => {
  const recentOrders = event.target.result;
  console.log(`Recent orders: ${recentOrders.length}`);
};

Compound Key Ranges

// Query with compound keys [category, price]
const categoryPriceIndex = store.index("category_price");

// Electronics under $1000
const electronicsRange = IDBKeyRange.bound(
  ["electronics", 0],
  ["electronics", 1000]
);

categoryPriceIndex.getAll(electronicsRange).onsuccess = (event) => {
  const affordableElectronics = event.target.result;
  console.log("Affordable electronics:", affordableElectronics);
};

// All books (any price)
const booksRange = IDBKeyRange.bound(
  ["books", Number.NEGATIVE_INFINITY],
  ["books", Number.POSITIVE_INFINITY]
);

// Or more simply for all books:
const allBooksRange = IDBKeyRange.bound(
  ["books"],
  ["books", []] // Empty array is greater than any other value
);

String Range Queries

// String prefix matching
const nameIndex = store.index("by_name");

// Products starting with "Lap" (like "Laptop")
const prefixRange = IDBKeyRange.bound("Lap", "Lap\uffff");

nameIndex.getAll(prefixRange).onsuccess = (event) => {
  const laptops = event.target.result;
  console.log("Products starting with 'Lap':", laptops);
};

// Case-insensitive range (if data is normalized to lowercase)
const searchTerm = "gaming";
const caseInsensitiveRange = IDBKeyRange.bound(
  searchTerm,
  searchTerm + "\uffff"
);

nameIndex.getAll(caseInsensitiveRange).onsuccess = (event) => {
  const gamingProducts = event.target.result;
  console.log("Gaming products:", gamingProducts);
};

Numeric Range Queries

// Price ranges for different budgets
const priceIndex = store.index("by_price");

const budgetRanges = {
  budget: IDBKeyRange.upperBound(100),           // <= $100
  midRange: IDBKeyRange.bound(100, 500, true),   // $100 < price <= $500
  premium: IDBKeyRange.bound(500, 2000, true),   // $500 < price <= $2000
  luxury: IDBKeyRange.lowerBound(2000, true)     // > $2000
};

Object.entries(budgetRanges).forEach(([category, range]) => {
  priceIndex.count(range).onsuccess = (event) => {
    console.log(`${category} products: ${event.target.result}`);
  };
});

Key Range Validation and Testing

// Test if key is in range
const priceRange = IDBKeyRange.bound(100, 500);

console.log(priceRange.includes(150));  // true
console.log(priceRange.includes(50));   // false
console.log(priceRange.includes(600));  // false
console.log(priceRange.includes(100));  // true (inclusive by default)
console.log(priceRange.includes(500));  // true (inclusive by default)

// Test with exclusive bounds
const exclusiveRange = IDBKeyRange.bound(100, 500, true, true);
console.log(exclusiveRange.includes(100)); // false (exclusive)
console.log(exclusiveRange.includes(500)); // false (exclusive)
console.log(exclusiveRange.includes(300)); // true

Complex Query Patterns

Multi-condition Queries with Cursors

// Complex filtering using cursor + key ranges
function findProductsWithConditions(store, conditions) {
  return new Promise((resolve) => {
    const results = [];
    const { category, minPrice, maxPrice, inStock } = conditions;
    
    // Use compound index for category + price
    const categoryPriceIndex = store.index("category_price");
    const range = IDBKeyRange.bound(
      [category, minPrice],
      [category, maxPrice]
    );
    
    categoryPriceIndex.openCursor(range).onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor) {
        const product = cursor.value;
        
        // Additional filtering in memory
        if (!inStock || product.stock > 0) {
          results.push(product);
        }
        
        cursor.continue();
      } else {
        resolve(results);
      }
    };
  });
}

// Usage
const tx = db.transaction("products", "readonly");
const store = tx.objectStore("products");

findProductsWithConditions(store, {
  category: "electronics",
  minPrice: 200,
  maxPrice: 1000,
  inStock: true
}).then((products) => {
  console.log("Filtered products:", products);
});

Pagination with Key Ranges

// Paginate within a key range
function getPaginatedRange(index, keyRange, pageSize, lastKey = null) {
  return new Promise((resolve) => {
    const results = [];
    let count = 0;
    
    // Adjust range to start after last key
    let effectiveRange = keyRange;
    if (lastKey) {
      if (keyRange.upper !== undefined) {
        effectiveRange = IDBKeyRange.bound(
          lastKey,
          keyRange.upper,
          true, // Exclude lastKey
          keyRange.upperOpen
        );
      } else {
        effectiveRange = IDBKeyRange.lowerBound(lastKey, true);
      }
    }
    
    index.openCursor(effectiveRange).onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor && count < pageSize) {
        results.push({
          key: cursor.key,
          value: cursor.value
        });
        count++;
        cursor.continue();
      } else {
        resolve({
          results,
          hasMore: cursor !== null,
          lastKey: results.length > 0 ? results[results.length - 1].key : null
        });
      }
    };
  });
}

// Usage - paginate expensive products
const priceIndex = store.index("by_price");
const expensiveRange = IDBKeyRange.lowerBound(1000);

async function paginateExpensiveProducts() {
  let lastKey = null;
  let page = 1;
  
  do {
    const result = await getPaginatedRange(priceIndex, expensiveRange, 10, lastKey);
    
    console.log(`Page ${page}:`, result.results.map(r => 
      `${r.value.name} - $${r.key}`
    ));
    
    lastKey = result.lastKey;
    page++;
  } while (result.hasMore);
}

Range Union Queries

// Simulate OR queries by combining multiple ranges
async function getMultipleRanges(index, ranges) {
  const allResults = [];
  
  for (const range of ranges) {
    const results = await new Promise((resolve) => {
      const rangeResults = [];
      
      index.openCursor(range).onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
          rangeResults.push(cursor.value);
          cursor.continue();
        } else {
          resolve(rangeResults);
        }
      };
    });
    
    allResults.push(...results);
  }
  
  // Remove duplicates based on primary key
  const uniqueResults = allResults.filter((product, index, array) => 
    array.findIndex(p => p.id === product.id) === index
  );
  
  return uniqueResults;
}

// Usage - get both budget and luxury products
const priceIndex = store.index("by_price");
const ranges = [
  IDBKeyRange.upperBound(100),      // Budget: <= $100
  IDBKeyRange.lowerBound(2000)      // Luxury: >= $2000
];

getMultipleRanges(priceIndex, ranges).then((products) => {
  console.log("Budget and luxury products:", products);
});

Performance Considerations

// Efficient key range usage
const priceIndex = store.index("by_price");

// Good - use specific range
const specificRange = IDBKeyRange.bound(100, 500);
priceIndex.getAll(specificRange);

// Better - use count() first for large datasets
priceIndex.count(specificRange).onsuccess = (event) => {
  const count = event.target.result;
  
  if (count < 1000) {
    // Small result set - get all
    priceIndex.getAll(specificRange);
  } else {
    // Large result set - use cursor with pagination
    priceIndex.openCursor(specificRange);
  }
};

// Best - combine with limits
priceIndex.getAll(specificRange, 50).onsuccess = (event) => {
  const firstFifty = event.target.result;
  // Process first 50 results
};

Error Handling with Key Ranges

// Handle invalid key ranges
try {
  // This will throw DataError - lower > upper
  const invalidRange = IDBKeyRange.bound(500, 100);
} catch (error) {
  if (error.name === "DataError") {
    console.error("Invalid key range - lower bound greater than upper bound");
  }
}

// Validate keys before creating ranges
function createSafeRange(lower, upper) {
  try {
    // Normalize and validate keys
    if (lower != null && upper != null) {
      // Simple comparison for same types
      if (typeof lower === typeof upper && lower > upper) {
        throw new Error("Lower bound cannot be greater than upper bound");
      }
    }
    
    return IDBKeyRange.bound(lower, upper);
  } catch (error) {
    console.error("Failed to create key range:", error.message);
    return null;
  }
}

// Usage
const range = createSafeRange(userInputMin, userInputMax);
if (range) {
  priceIndex.getAll(range);
}