Syntax & Basics

Learn the fundamental SDBQL syntax including query structure, pipeline operator, null coalescing, optional chaining, CASE expressions, template strings, and window functions.

Basic Syntax

SDBQL queries follow a declarative structure that reads like a sentence.

LET variable = expression
FOR variable IN collection
  FILTER expression
  SORT field [ASC|DESC]
  LIMIT [offset,] count
  RETURN expression

LET Variable Assignment

Binds values or subquery results to variables.

FOR Iteration

Iterates over collections or arrays.

FILTER Filtering

Restricts results based on conditions.

RETURN Projection

Constructs the result documents.

.. Range Expressions

Create numeric arrays using the range operator start..end (inclusive on both ends).

1..5 -> [1, 2, 3, 4, 5]

Basic Range

-- Returns [1, 2, 3, 4, 5]
RETURN 1..5

-- Negative numbers
RETURN -2..2  -- [-2, -1, 0, 1, 2]

In FOR Loop

FOR i IN 1..10
  RETURN i * 2

-- With expressions
LET n = 5
FOR i IN 1..n
  RETURN i

Batch Insert with Range

FOR i IN 1..100
  INSERT { index: i, name: CONCAT("Item", i) } INTO items
  RETURN i

|> Pipeline Operator NEW

Chain function calls for readable data transformations. The |> operator passes the left value as the first argument to the right-side function.

-- String transformations
RETURN "  hello world  " |> TRIM() |> UPPER() |> SPLIT(" ")
-- Result: ["HELLO", "WORLD"]

-- Array operations
RETURN [3, 1, 4, 1, 5] |> UNIQUE() |> SORTED() |> REVERSE() |> FIRST()
-- Result: 5

-- With extra arguments
RETURN 3.14159 |> ROUND(2) |> TO_STRING()
-- Result: "3.14"

Lambda Expressions

Use x -> expression syntax for inline transformations with higher-order functions like FILTER, MAP, ANY, and ALL.

FILTER with Lambda
[1, 2, 3, 4, 5] |> FILTER(x -> x > 2)
-- Result: [3, 4, 5]

-- Complex condition
items |> FILTER(i -> i.price > 50 AND i.stock > 0)
MAP with Lambda
[1, 2, 3] |> MAP(x -> x * 2)
-- Result: [2, 4, 6]

-- Extract fields
users |> MAP(u -> u.name)
ANY / ALL
[1, 2, 3] |> ANY(x -> x > 2)  -- true
[1, 2, 3] |> ALL(x -> x > 0)  -- true
REDUCE
-- Sum: (accumulator, item) -> expr, initial
[1, 2, 3, 4, 5] |> REDUCE((acc, x) -> acc + x, 0)
-- Result: 15

-- Concatenate strings
["a", "b", "c"] |> REDUCE((acc, x) -> CONCAT(acc, x), "")
-- Result: "abc"
Chaining Pipeline Operations
-- Filter, transform, and aggregate in one chain
orders
  |> FILTER(o -> o.status == "pending")
  |> MAP(o -> o.total)
  |> REDUCE((sum, t) -> sum + t, 0)
-- Returns total of pending orders

Supported Functions: FIRST, LAST, LENGTH, REVERSE, SORTED, UNIQUE, FLATTEN, UPPER, LOWER, TRIM, SPLIT, ROUND, ABS, FLOOR, CEIL, TO_STRING, TO_NUMBER, FILTER, MAP, ANY, ALL, FIND, REDUCE

?? Null Coalescing Operator NEW

The ?? operator returns the left operand if it's not null, otherwise evaluates and returns the right operand. Provides a concise way to handle null values.

-- Basic usage
RETURN null ?? "default"       -- Result: "default"
RETURN "value" ?? "default"    -- Result: "value"

-- Chaining multiple fallbacks
RETURN null ?? null ?? "last"  -- Result: "last"

-- With object fields
FOR doc IN users
  RETURN doc.nickname ?? doc.name ?? "Anonymous"
Difference from COALESCE()

?? is syntax sugar for the COALESCE function but with short-circuit evaluation.

-- These are equivalent:
doc.field ?? "default"
COALESCE(doc.field, "default")
Non-null Falsy Values

Only null triggers fallback. Other falsy values pass through.

0 ?? 42         -- Result: 0
"" ?? "default" -- Result: ""
false ?? true   -- Result: false
Combine with Pipeline
-- Get first item or default to 0
RETURN ([] |> FIRST()) ?? 0    -- Result: 0

-- Process with fallback
items |> FILTER(x -> x.active) |> FIRST() ?? { name: "None" }

|| Logical OR Operator NEW

The || operator returns the left operand if it's truthy, otherwise evaluates and returns the right operand. Use this when you want to fall back on any falsy value (null, false, 0, "").

-- Basic usage
RETURN null || "default"       -- Result: "default"
RETURN false || "default"      -- Result: "default"
RETURN 0 || 42                 -- Result: 42
RETURN "" || "default"         -- Result: "default"

-- Non-falsy values pass through
RETURN "value" || "default"    -- Result: "value"
RETURN 1 || 42                 -- Result: 1
?? vs || Comparison

Use ?? when only null should trigger fallback. Use || when any falsy value should trigger fallback.

Expression ?? (null coalesce) || (logical OR)
null _ 0 0 0
false _ 0 false 0
0 _ 42 0 42
"" _ "default" "" "default"
"value" _ "default" "value" "value"
Common Use Cases
-- Default for empty strings
RETURN doc.nickname || doc.name || "Anonymous"

-- Default for zero values
RETURN doc.count || 1

-- Use in aggregations
RETURN SUM(FOR i IN items RETURN i.qty) || 0

?. Optional Chaining NEW

The ?. operator safely accesses nested properties. If the base is null or not an object, it returns null instead of throwing an error.

-- Safe nested access
FOR doc IN users
  RETURN {
    name: doc.name,
    city: doc.address?.city,           -- null if no address
    lat: doc.address?.location?.lat    -- null if address or location missing
  }

-- Chained optional access
doc.profile?.settings?.theme?.color    -- null at any missing level
Without Optional Chaining
-- Verbose null checks
doc.address != null
  ? (doc.address.location != null
      ? doc.address.location.lat
      : null)
  : null
With Optional Chaining
-- Clean and concise
doc.address?.location?.lat
Combine with ?? for Defaults
-- Optional chaining + null coalescing
FOR doc IN users
  RETURN {
    name: doc.name,
    city: doc.address?.city ?? "Unknown",
    theme: doc.profile?.settings?.theme ?? "light"
  }
Use in FILTER
-- Filter safely with optional chaining
FOR doc IN users
  FILTER doc.address?.city == "New York"
  RETURN doc

-- Find users with missing data
FOR doc IN users
  FILTER doc.address?.city == null
  RETURN doc.name

Note: ?. returns null for non-object types (arrays, strings, numbers). For array element access, use regular bracket notation with null checks.

CASE CASE/WHEN Expressions NEW

SQL-style conditional expressions with two forms: Simple CASE for value matching and Searched CASE for condition evaluation.

Simple CASE
-- Match expression against values
CASE doc.category
    WHEN "electronics" THEN "Tech"
    WHEN "books" THEN "Reading"
    WHEN "clothing" THEN "Fashion"
    ELSE "Other"
END
Searched CASE
-- Evaluate conditions
CASE
    WHEN doc.price >= 500 THEN "premium"
    WHEN doc.price >= 100 THEN "mid-range"
    WHEN doc.price >= 50 THEN "budget"
    ELSE "bargain"
END
Practical Example
FOR doc IN products
  RETURN {
    name: doc.name,
    price_tier: CASE
      WHEN doc.price >= 500 THEN "premium"
      WHEN doc.price >= 100 THEN "mid-range"
      ELSE "budget"
    END,
    availability: CASE
      WHEN doc.stock == 0 THEN "Out of Stock"
      WHEN doc.stock < 50 THEN "Low Stock"
      ELSE "In Stock"
    END
  }
Use in LET for Reuse
FOR doc IN products
  LET tier = CASE
    WHEN doc.price >= 100 THEN "high"
    ELSE "low"
  END
  FILTER tier == "high"
  RETURN doc
Custom Sort Order
FOR doc IN tasks
  LET priority = CASE doc.status
    WHEN "urgent" THEN 1
    WHEN "high" THEN 2
    WHEN "normal" THEN 3
    ELSE 4
  END
  SORT priority
  RETURN doc

Note: CASE returns null if no WHEN matches and no ELSE is provided. WHEN clauses are evaluated in order - first match wins.

$"" String Interpolation NEW

Template strings with ${expression} syntax for cleaner string building. Use $"..." or $'...' prefix.

Before (Verbose)
RETURN CONCAT("Hello, ", doc.name, "! You have ", TO_STRING(doc.points), " points.")
After (Clean)
RETURN $"Hello, ${doc.name}! You have ${doc.points} points."
Expressions Inside ${...}
FOR doc IN products
  RETURN {
    -- Simple field access
    greeting: $"Welcome, ${doc.name}!",

    -- Arithmetic expressions
    total: $"Total: $$${doc.price * doc.qty}",

    -- Function calls
    formatted: $"Name: ${UPPER(doc.name)}",

    -- Ternary expressions
    status: $"Status: ${doc.active ? 'Active' : 'Inactive'}",

    -- Optional chaining
    city: $"City: ${doc.address?.city ?? 'Unknown'}"
  }
Type Coercion
-- Numbers, booleans, null auto-convert
$"Count: ${42}"         -- "Count: 42"
$"Active: ${true}"      -- "Active: true"
$"Value: ${null}"       -- "Value: null"

-- Arrays and objects serialize to JSON
$"Tags: ${['a','b']}"   -- 'Tags: ["a","b"]'
Escape Sequences
-- Literal dollar sign
$"Price: $$50"          -- "Price: $50"

-- Literal ${
$"Code: \${x}"          -- "Code: ${x}"

-- Standard escapes
$"Line1\nLine2"         -- newline
$"Col1\tCol2"           -- tab

Note: String interpolation is secure - expressions are parsed at compile time as AST nodes, not evaluated as strings at runtime. User data cannot inject code.

OVER Window Functions NEW

Window functions perform calculations across rows related to the current row without collapsing them. Use OVER clause with optional PARTITION BY and ORDER BY to define the window.

-- Row numbering
FOR doc IN sales
  RETURN {
    date: doc.date,
    row_num: ROW_NUMBER() OVER (ORDER BY doc.date)
  }

-- Partitioned row numbers (restart per region)
FOR doc IN sales
  RETURN {
    region: doc.region,
    row_in_region: ROW_NUMBER() OVER (PARTITION BY doc.region ORDER BY doc.date)
  }

-- Running total
FOR doc IN sales
  RETURN {
    date: doc.date,
    amount: doc.amount,
    running_total: SUM(doc.amount) OVER (ORDER BY doc.date)
  }
Ranking Functions
-- ROW_NUMBER: Sequential numbers
ROW_NUMBER() OVER (ORDER BY score DESC)
-- Result: 1, 2, 3, 4, 5...

-- RANK: Same rank for ties, gaps after
RANK() OVER (ORDER BY score DESC)
-- Scores: 100, 100, 90 -> Ranks: 1, 1, 3

-- DENSE_RANK: Same rank for ties, no gaps
DENSE_RANK() OVER (ORDER BY score DESC)
-- Scores: 100, 100, 90 -> Ranks: 1, 1, 2
Navigation Functions
-- LAG: Access previous row value
LAG(doc.amount) OVER (ORDER BY doc.date)

-- LEAD: Access next row value
LEAD(doc.amount) OVER (ORDER BY doc.date)

-- FIRST_VALUE: First value in partition
FIRST_VALUE(doc.amount) OVER (
  PARTITION BY doc.region ORDER BY doc.date
)

-- LAST_VALUE: Last value in partition
LAST_VALUE(doc.amount) OVER (
  PARTITION BY doc.region ORDER BY doc.date
)
Running Aggregates

Use aggregate functions with OVER for cumulative calculations.

FOR doc IN transactions
  FILTER doc.account_id == "acc123"
  SORT doc.date
  RETURN {
    date: doc.date,
    amount: doc.amount,
    running_sum: SUM(doc.amount) OVER (ORDER BY doc.date),
    running_avg: AVG(doc.amount) OVER (ORDER BY doc.date),
    running_count: COUNT(doc._key) OVER (ORDER BY doc.date),
    running_min: MIN(doc.amount) OVER (ORDER BY doc.date),
    running_max: MAX(doc.amount) OVER (ORDER BY doc.date)
  }
Practical Example: Compare to Previous
-- Calculate day-over-day change
FOR doc IN daily_sales
  SORT doc.date
  LET prev = LAG(doc.revenue) OVER (ORDER BY doc.date)
  RETURN {
    date: doc.date,
    revenue: doc.revenue,
    prev_revenue: prev,
    change: prev != null ? doc.revenue - prev : null,
    pct_change: prev != null ? ROUND((doc.revenue - prev) / prev * 100, 2) : null
  }

Supported Functions: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, FIRST_VALUE, LAST_VALUE, SUM, AVG, COUNT, MIN, MAX

JOIN JOIN Operations NEW

Combine data from multiple collections efficiently using JOIN, LEFT/RIGHT JOIN, and FULL OUTER JOIN. SDBQL uses document-oriented join semantics where matching documents are grouped into arrays.

-- Basic INNER JOIN
FOR user IN users
  JOIN orders ON user._key == orders.user_key
  RETURN {
    user: user.name,
    orders: orders  -- matched orders array
  }

-- LEFT JOIN (include users without orders)
FOR user IN users
  LEFT JOIN orders ON user._key == orders.user_key
  RETURN {
    user: user.name,
    orders: orders  -- empty array if no matches
  }

-- RIGHT JOIN (all orders with matching users)
FOR user IN users
  RIGHT JOIN orders ON user._key == orders.user_key
  RETURN {
    order: orders._key,
    user: user  -- array containing the matched user(s)
  }

-- FULL OUTER JOIN (union of all rows)
FOR user IN users
  FULL OUTER JOIN orders ON user._key == orders.user_key
  RETURN {
    user_name: user?.name,
    order_id: orders?._key
  }
Key Features
  • Support for INNER, LEFT, RIGHT, and FULL joins
  • Join condition using ON clause
  • Multiple sequential joins supported
  • Document-oriented results (nested arrays)
Complex Example
FOR u IN users
  JOIN orders ON u._key == orders.user_key
  LEFT JOIN reviews ON u._key == reviews.user
  RETURN {
    name: u.name,
    spent: SUM(orders[*].total),
    review_count: LENGTH(reviews)
  }

Note: The variable name for the joined collection is automatically derived from the collection name.

LET & Subqueries

Correlated Subqueries

Use LET inside a FOR loop to perform powerful joins and aggregations.

FOR u IN users
  -- Subquery accessing outer variable 'u'
  LET userOrders = (
    FOR o IN orders
    FILTER o.user == u.name
    RETURN o
  )
  RETURN { name: u.name, orders: userOrders }

Aggregation with Subqueries

FOR u IN users
  LET totalSpent = SUM((
    FOR o IN orders
    FILTER o.user == u.name
    RETURN o.amount
  ))
  FILTER totalSpent > 100
  RETURN { name: u.name, spent: totalSpent }

BULK Bulk Operations NEW

Perform efficient batch operations like INSERT, UPDATE, DELETE, and UPSERT on collections.

-- Bulk INSERT
INSERT { name: "New User 1", email: "[email protected]" } INTO users
INSERT { name: "New User 2", email: "[email protected]" } INTO users

-- Bulk UPDATE
FOR u IN users
  FILTER u.status == "pending"
  UPDATE u WITH { status: "active", updated_at: DATE_NOW() } IN users

-- Bulk DELETE
FOR o IN old_orders
  FILTER o.date < DATE_SUBTRACT(DATE_NOW(), 1, "year")
  REMOVE o IN old_orders

-- Bulk UPSERT (Update or Insert)
UPSERT { email: "[email protected]" }
  INSERT { name: "Example User", email: "[email protected]", created_at: DATE_NOW() }
  UPDATE { last_login: DATE_NOW() }
  IN users
Key Features
  • High-performance batch operations
  • Atomic operations for data consistency
  • Flexible filtering for targeted changes
  • UPSERT for conditional inserts/updates
Return Modified Documents
FOR u IN users
  FILTER u.status == "new"
  UPDATE u WITH { status: "active" } IN users
  RETURN NEW

Use RETURN NEW or RETURN OLD to get the state of documents before or after modification.