> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fiquela.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Fluent API

> Build FiQueLa queries with a chainable PHP API. Select fields, filter rows, join files, group, sort, and paginate — all in one expression.

The fluent API lets you build queries by chaining PHP method calls. Every method returns the query object, so calls can be composed in any order.

<Note>
  As of FiQueLa **3.0**, the fluent helpers are **expression-first**: `select`, `where`, `having`, `groupBy`, `orderBy`, and every scalar / aggregate helper accept full SQL expression strings. They route their field arguments through the same parser that powers FQL, so `$query->select('ROUND(price * 1.21, 2) AS gross')`, `$query->groupBy('YEAR(date)')`, `$query->where('LOWER(name)', Op::EQ, 'alice')`, and `$query->sum('price + vat')` all work end-to-end. Infix arithmetic (`+ - * / %`) and nested function calls are valid in every clause.
</Note>

```php theme={null}
use FQL\Query;
use FQL\Enum\Operator;

$results = Query\Provider::fromFileQuery('json(./products.json).data.products')
    ->select('id', 'name', 'brand.code')
    ->where('price', Operator::GREATER_THAN, 100)
    ->orderBy('price')->desc()
    ->limit(20)
    ->execute()
    ->fetchAll();
```

<Note>
  Every query implements `\Stringable`. Cast a query to a string to see the equivalent FQL representation — useful for debugging.

  ```php theme={null}
  echo (string) $query;
  ```
</Note>

## Field path syntax

FiQueLa uses a dot-separated path to reference fields in nested data structures. Beyond simple dot access, the path syntax supports several advanced features:

| Syntax               | Description                                                       | Example                    |
| -------------------- | ----------------------------------------------------------------- | -------------------------- |
| `a.b.c`              | Standard nested access                                            | `brand.code`               |
| `a.b.0.c`            | Indexed access (numeric key)                                      | `items.0.name`             |
| `a.b[].c`            | Iterated access — traverses all elements of an array              | `items[].price`            |
| `a.b.*.c`            | Wildcard — expands all keys of an associative array               | `attributes.*.value`       |
| `` `key.with.dot` `` | Backtick-escaped key — treats the enclosed string as a single key | `` `Prod. cena`.`s dph` `` |
| `x[0]`               | Index into a scalar or wrapped value                              | `tags[0]`                  |

Backtick escaping is useful when a key contains dots, spaces, or other special characters that would otherwise be interpreted as path separators.

```php theme={null}
// Standard nested access
$query->select('brand.code')->as('brandCode');

// Iterated access — collect a value from every element in an array
$query->select('items[].price')->as('prices');

// Escaped key containing a dot
$query->select('`price.vat`')->as('priceVat');
```

```sql theme={null}
-- FQL equivalent
SELECT brand.code AS brandCode,
       items[].price AS prices,
       `price.vat` AS priceVat
FROM json(products.json).data.products
```

This path syntax works everywhere a field name is expected: `select()`, `where()`, `orderBy()`, `groupBy()`, `on()`, and inside function arguments. Scalar functions accept `[]` paths and operate on the flattened array — for example, `$query->length('products.product[]')` returns the element count, and `$query->implode('products.product[].name', ', ')` joins every nested name.

<Note>
  Prior to FiQueLa **3.0.2**, scalar functions wrapping a `[]` path returned `null`. Upgrade to 3.0.2 or later to use array-iterator paths inside scalar functions.
</Note>

## Selecting fields

Use `select()` to specify which fields to include. Dot notation accesses nested fields. Multiple `select()` calls merge their fields.

```php theme={null}
// All equivalent
$query->select('id, name, address.city, address.state');
$query->select('id', 'name', 'address.city', 'address.state');
$query->select('id', 'name')->select('address.city', 'address.state');
```

Use `selectAll()` to select all fields (equivalent to `SELECT *`). You can combine it with additional fields:

```php theme={null}
$query->selectAll()->select('totalPrice');
```

### Aliases

Chain `as()` after `select()`, `from()`, or a join method to create an alias. The `as()` method is context-aware — it detects what was called before it and aliases accordingly:

| Context          | Example                            | Effect                          |
| ---------------- | ---------------------------------- | ------------------------------- |
| After `select()` | `->select('id')->as('clientId')`   | Aliases the last selected field |
| After `from()`   | `->from('data.products')->as('p')` | Aliases the FROM source         |
| After `join()`   | `->leftJoin($query)->as('o')`      | Aliases the joined source       |

```php theme={null}
$query->select('id')->as('clientId');
$query->select('brand.code')->as('brandCode');
```

<Note>
  When used after `select()`, `as()` only aliases the **last** field in the preceding call. For example, `select('id', 'name')->as('o')` creates `id, name AS o`.
</Note>

### DISTINCT

Remove duplicate rows with `distinct()`:

```php theme={null}
$query->select('category')->distinct();
```

### EXCLUDE

Use `exclude()` to remove fields from the output — useful when applying functions and you want only the computed result:

```php theme={null}
$query->select('id', 'name')
    ->round('totalPrice', 2)->as('finalPrice')
    ->exclude('totalPrice');

// Output: id, name, finalPrice (totalPrice removed)
```

### Aliased wildcards

When a source has an alias (from `from()->as()` or a join), use `alias.*` in `select()` to select all fields from that specific source:

```php theme={null}
$query->from('data.products')->as('p')
    ->select('p.*')
    ->leftJoin($orders)->as('o')
        ->on('p.id', Operator::EQUAL, 'product_id')
    ->select('o.total_price')->as('totalPrice');
```

If two aliased sources share field names and you use `alias.*` for both, a `SelectException` is thrown on ambiguous field conflicts.

## Specifying the data path

Use `from()` to point to the data root within the file. Dot notation traverses nested structures:

```php theme={null}
$query->from('data.products');
$query->from('SHOP.SHOPITEM');
```

When using `Query\Provider::fromFileQuery()`, the path embedded in the FileQuery string sets the initial `from`.

### FROM aliasing

Chain `as()` after `from()` to alias the data source. Aliased fields are then accessible via `alias.field` dot notation:

```php theme={null}
$query->from('data.products')->as('p')
    ->select('p.id', 'p.name', 'p.price')
    ->where('p.price', Operator::GREATER_THAN, 100);
```

This is equivalent to the FQL syntax `FROM json(products.json).data.products AS p`.

## Pagination and limits

```php theme={null}
// Return at most 20 rows starting from row 40
$query->offset(40)->limit(20);

// Page-based helper — page 2, 20 rows per page
$query->page(2, perPage: 20);
```

## Sorting

Chain `orderBy()` with `asc()` or `desc()`. Multiple `orderBy()` calls define a multi-column sort. Dot notation is supported for sorting by nested fields.

```php theme={null}
use FQL\Enum\Sort;

$query->orderBy('price')->desc();
$query->orderBy('price', Sort::ASC)->orderBy('name', Sort::DESC);

// Sort by nested field
$query->orderBy('brand.code')->asc();
```

## Conditions

Filter rows with `where()`, then chain `and()`, `or()`, or `xor()`:

```php theme={null}
use FQL\Enum\Operator;

$query->where('price', Operator::GREATER_THAN, 100)
    ->and('description', Operator::LIKE, '%wireless%')
    ->or('price', Operator::BETWEEN, [300, 500]);
```

See [Conditions](/querying/conditions) for the full operator reference and condition grouping.

## Joins

Join other files or queries using `innerJoin()`, `leftJoin()`, `rightJoin()`, or `fullJoin()`, followed by `on()` to define the join condition. You can pass the alias as a second parameter or chain `->as()` fluently:

```php theme={null}
$orders = Query\Provider::fromFileQuery('xml(orders.xml).orders.order');

// Alias as parameter
$query->leftJoin($orders, 'o')
    ->on('id', Operator::EQUAL, 'user_id');

// Alias via as() — equivalent
$query->leftJoin($orders)->as('o')
    ->on('id', Operator::EQUAL, 'user_id');
```

See [Joining data sources](/querying/joins) for full details.

## Grouping and aggregations

Group rows with `groupBy()` and apply aggregate functions:

```php theme={null}
$query->count('id')->as('total')
    ->sum('price')->as('revenue')
    ->groupBy('category.id')
    ->having('total', Operator::GREATER_THAN, 10);
```

See [Grouping and aggregations](/querying/grouping-aggregations) for all aggregate functions.

## Common table expressions (WITH)

Register a named subquery on the parent query with `with()`. The CTE becomes visible to any `JOIN` or `UNION` branch composed on the parent.

```php theme={null}
use FQL\Enum\Operator;
use FQL\Query;

$expensive = Query\Provider::fromFileQuery('json(./products.json).data.products')
    ->select('id, name, price')
    ->where('price', Operator::GREATER_THAN, 400);

$query = Query\Provider::fromFileQuery('json(./users.json).data.users')
    ->with('expensive', $expensive);

$results = $query
    ->leftJoin($query->getCte('expensive'))->as('expensive')
    ->on('id', Operator::EQUAL, 'expensive.user_id')
    ->execute()
    ->fetchAll();
```

Available helpers: `with(string $name, Query $query)`, `hasCte(string $name)`, `getCte(string $name)`, `getCtes()`.

See [Common table expressions](/advanced/common-table-expressions) for full semantics, evaluation strategy, and limitations.

## UNION

Combine results from multiple queries:

```php theme={null}
// Remove duplicates
$results = $query1->union($query2)->execute();

// Keep all rows
$results = $query1->unionAll($query2)->execute();
```

The number of selected columns must match across all combined queries (queries using `SELECT *` skip this check).

## EXPLAIN

Inspect the query execution plan without running the full query, or run it with timing data:

```php theme={null}
// Plan only — no data processed
$plan = $query->explain()->execute();

// Execute and collect real row counts and timings
$plan = $query->explainAnalyze()->execute();
```

See [EXPLAIN & benchmarking](/advanced/explain-analyze) for the full column reference.

## DESCRIBE

Use `describe()` to inspect the schema of a data source instead of querying its data. The result is a `DescribeResult` containing one row per column with type statistics, completeness, and uniqueness information.

`DESCRIBE` is mutually exclusive with `SELECT`, `WHERE`, `GROUP BY`, `ORDER BY`, `LIMIT`, `JOIN`, `UNION`, and `EXPLAIN`. Calling any of these after `describe()` throws a `QueryLogicException`.

```php theme={null}
use FQL\Query\Provider;
use FQL\Stream\Json;

$result = Json::open('products.json')->query()
    ->from('data.products')
    ->describe()
    ->execute();

foreach ($result->fetchAll() as $col) {
    echo $col['column'] . ' => ' . $col['dominant'] . PHP_EOL;
}

echo 'Source rows: ' . $result->getSourceRowCount();
```

### Output columns

| Column         | Type      | Description                                                                                                                        |
| -------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `column`       | string    | Column name (dot notation for nested objects)                                                                                      |
| `path`         | string\[] | Array of path segments to the column. Useful for correctly evaluating nested fields whose keys contain dots or special characters. |
| `types`        | array     | Map of type name to occurrence count                                                                                               |
| `totalRows`    | int       | Number of non-empty rows for this column                                                                                           |
| `totalTypes`   | int       | Number of distinct types observed                                                                                                  |
| `dominant`     | string    | Most frequent type                                                                                                                 |
| `suspicious`   | bool      | `true` if column has mixed non-empty types (except int+double)                                                                     |
| `confidence`   | float     | Ratio of dominant type occurrences to total (0.0–1.0)                                                                              |
| `completeness` | float     | Ratio of non-empty rows to total rows (0.0–1.0)                                                                                    |
| `constant`     | bool      | `true` if all non-empty values are identical                                                                                       |
| `isEnum`       | bool      | `true` if column has 2–5 unique values                                                                                             |
| `isUnique`     | bool      | `true` if all non-empty values are unique                                                                                          |

### Detected types

| Type           | Description                       |
| -------------- | --------------------------------- |
| `int`          | Integer value                     |
| `double`       | Float/double value                |
| `string`       | Non-empty string                  |
| `bool`         | Boolean value                     |
| `array`        | Indexed array                     |
| `null`         | Null value                        |
| `empty-string` | Empty string `""`                 |
| `whitespace`   | String containing only whitespace |
| `bool-string`  | String `"yes"` or `"no"`          |
| `date-string`  | ISO 8601 date/datetime string     |

<Warning>
  `DESCRIBE` cannot be combined with other query clauses. Attempting to chain `select()`, `where()`, `groupBy()`, `orderBy()`, `limit()`, `join()`, `union()`, or `explain()` with `describe()` throws a `QueryLogicException`.
</Warning>

## Fluent API vs FQL string

<CodeGroup>
  ```php Fluent API theme={null}
  use FQL\Query;
  use FQL\Enum\Operator;

  $query = Query\Provider::fromFileQuery('json(products.json).data.products')
      ->select('brand.code')->as('brandCode')
      ->groupConcat('id', '/')->as('products')
      ->sum('price')->as('totalPrice')
      ->count('id')->as('productCount')
      ->where('price', Operator::LESS_THAN, 300)
      ->or('price', Operator::GREATER_THAN, 400)
      ->groupBy('brand.code')
      ->orderBy('productCount')->desc();
  ```

  ```sql FQL string theme={null}
  SELECT
    brand.code AS brandCode,
    GROUP_CONCAT(id, "/") AS products,
    SUM(price) AS totalPrice,
    COUNT(id) AS productCount
  FROM json(products.json).data.products
  WHERE
    price < 300
    OR price > 400
  GROUP BY brand.code
  ORDER BY productCount DESC
  ```
</CodeGroup>

Casting the fluent query to a string produces the FQL representation, so you can always inspect what SQL-like string a fluent query corresponds to.
