froe/

The repository.
Directly from disk.

Froe is a Rust CLI and library for reading and maintaining Oak segment stores, including those used by AEM.

Inspect, export, compare revisions and maintain offline.
No running Oak instance. No JVM.

Working with a store

Read-only · live or stopped repository
# Inspect the store and traverse content
froe summary /path/to/segmentstore
froe tree /path/to/segmentstore /content --depth 2

# Find consistent revisions
froe check /path/to/segmentstore
journal.log / data*.tarsegment store directory
READ

No repository writes. No lock.

Traverse nodes, inspect segments, diff revisions, trace history and export typed properties as JSON lines, Parquet or SQLite.

WRITE

Stopped repository. Exclusive lock.

Compact, back up, restore, recover journals and manage checkpoints; mutating operations require confirmation.

DOWNLOAD

Prebuilt binaries.
Latest release.

v0.12.0 for Linux, macOS and Windows; verify archives against SHA256SUMS.

Release notes
BUILD FROM SOURCE

One binary.
A library, too.

The CLI includes Parquet and SQLite export support; the core crate exposes the repository traversal API.

Quick start in the README
shell
git clone https://github.com/koraytaylan/froe.git
cd froe
cargo build --release
./target/release/froe --help
Core API: docs.rs/froe

Repository operations

Audit a content snapshot

Export nodes and typed properties, then query locally in DuckDB or SQLite; repeat Parquet exports decode changed subtrees.

SQL examples

Recover a missing journal

Use check to inspect consistency, then recover-journal to reconstruct the journal from surviving segments with Oak stopped.

Recovery workflow

SQL over an AEM repository export

Export the whole tree once, then query the files; these examples read exported data and do not modify the repository.

# Choose the format for your SQL client
froe export /path/to/segmentstore --format parquet --output ./export
froe export /path/to/segmentstore --format sqlite --output ./export.db
nodes · properties · typed values
AEM page counts by site

Count cq:Page nodes under /content, grouped by the first path component beneath it.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT
    regexp_extract(path, '^(/content/[^/]+)', 1) AS site,
    count(*) AS pages,
    rank() OVER (ORDER BY count(*) DESC) AS rank
  FROM './export/nodes.parquet'
  WHERE primary_type = 'cq:Page'
    AND path LIKE '/content/%'
  GROUP BY 1
  ORDER BY pages DESC;"
AEM pages using a component

List pages whose content contains a component with the given sling:resourceType; replace mysite/components/teaser with yours.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT DISTINCT n.path AS page
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON n.path = split_part(p.path, '/jcr:content/', 1)
  WHERE p.name = 'sling:resourceType'
    AND p.value = 'mysite/components/teaser'
    AND n.primary_type = 'cq:Page'
  ORDER BY 1;"
AEM pages using a template

List pages whose jcr:content names the given cq:template; editable templates live under /conf, static ones under /apps.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT n.path AS page
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON p.path = n.path || '/jcr:content'
  WHERE p.name = 'cq:template'
    AND p.value = '/conf/mysite/settings/wcm/templates/article-page'
    AND n.primary_type = 'cq:Page'
  ORDER BY 1;"
AEM page counts by template

Count pages per cq:template; templates no page uses do not appear.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT p.value AS template, count(*) AS pages
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON p.path = n.path || '/jcr:content'
  WHERE p.name = 'cq:template'
    AND n.primary_type = 'cq:Page'
  GROUP BY 1
  ORDER BY pages DESC, 1;"
AEM pages with a tag

cq:tags is multivalued, so each tag is its own row; match the form your repository stores, a tag ID or a /content/cq:tags path.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT n.path AS page
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON p.path = n.path || '/jcr:content'
  WHERE p.name = 'cq:tags'
    AND p.value = 'mysite:topics/news'
    AND n.primary_type = 'cq:Page'
  ORDER BY 1;"
AEM pages referencing an asset

Find pages with a property whose value is exactly the asset path, such as fileReference; links inside rich text are not matched.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT DISTINCT n.path AS page, p.name AS property
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON n.path = split_part(p.path || '/', '/jcr:content/', 1)
  WHERE p.value = '/content/dam/mysite/logo.svg'
    AND n.primary_type = 'cq:Page'
  ORDER BY 1, 2;"
Experience fragments on AEM pages

Map each fragmentVariationPath placed in page content to its page; fragments in editable template structure live under /conf and are not listed.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT p.value AS fragment, n.path AS page
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON n.path = split_part(p.path, '/jcr:content/', 1)
  WHERE p.name = 'fragmentVariationPath'
    AND n.primary_type = 'cq:Page'
  ORDER BY 1, 2;"
AEM pages not modified since a date

Compare cq:lastModified as a timestamp, so time zone offsets are honored; pages never edited have no cq:lastModified and are not listed.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT n.path AS page, p.value AS last_modified
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON p.path = n.path || '/jcr:content'
  WHERE p.name = 'cq:lastModified'
    AND p.property_type = 'Date'
    AND n.primary_type = 'cq:Page'
    AND CAST(p.value AS TIMESTAMPTZ) < TIMESTAMPTZ '2025-01-01 00:00:00+00'
  ORDER BY CAST(p.value AS TIMESTAMPTZ);"
Last editors by page count

Group pages by cq:lastModifiedBy; only the most recent editor is recorded on the page, earlier ones live in version history.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT p.value AS editor, count(*) AS pages
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON p.path = n.path || '/jcr:content'
  WHERE p.name = 'cq:lastModifiedBy'
    AND n.primary_type = 'cq:Page'
  GROUP BY 1
  ORDER BY pages DESC, 1;"
AEM pages without a description

Find site pages with no non-empty jcr:description, the page property most page components render as the meta description.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT n.path AS page
  FROM './export/nodes.parquet' n
  WHERE n.primary_type = 'cq:Page'
    AND n.path LIKE '/content/%'
    AND n.path NOT LIKE '/content/experience-fragments/%'
    AND NOT EXISTS (
      SELECT 1 FROM './export/properties.parquet' p
      WHERE p.path = n.path || '/jcr:content'
        AND p.name = 'jcr:description'
        AND p.value <> ''
    )
  ORDER BY 1;"
AEM pages that redirect

List cq:redirectTarget values and whether the target is in the export; external URLs and partial exports report false.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT
    n.path AS page,
    p.value AS target,
    t.path IS NOT NULL AS target_in_export
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' n
    ON p.path = n.path || '/jcr:content'
  LEFT JOIN './export/nodes.parquet' t
    ON t.path = p.value
  WHERE p.name = 'cq:redirectTarget'
    AND n.primary_type = 'cq:Page'
  ORDER BY 1;"
Oak index definitions

Inventory index type, async lane and reindex flag; froe index list provides the dedicated store-level view.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT
    n.path,
    max(CASE WHEN p.name = 'type' THEN p.value END) AS index_type,
    group_concat(DISTINCT CASE WHEN p.name = 'async' THEN p.value END) AS lane,
    max(CASE WHEN p.name = 'reindex' THEN p.value END) AS reindex
  FROM './export/nodes.parquet' n
  JOIN './export/properties.parquet' p
    ON p.path = n.path
  WHERE n.primary_type = 'oak:QueryIndexDefinition'
  GROUP BY 1
  ORDER BY 1;"
DAM assets without path references

Candidates with no external property value naming the asset or a descendant; UUID, embedded and external references are not covered, so this is not a deletion list.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT a.path
  FROM './export/nodes.parquet' a
  WHERE a.primary_type = 'dam:Asset'
    AND NOT EXISTS (
      SELECT 1 FROM './export/properties.parquet' p
      WHERE (p.value = a.path
        OR substr(p.value, 1, length(a.path) + 1) = a.path || '/')
        AND p.path <> a.path
        AND substr(p.path, 1, length(a.path) + 1) <> a.path || '/'
    )
  ORDER BY a.path;"
DAM assets by MIME type

Group assets by dc:format with the original renditions' sizes; external binaries have no recorded length, so they are counted separately.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT
    m.value AS mime_type,
    count(*) AS assets,
    sum(o.binary_length) AS inline_bytes,
    count(o.binary_reference) AS external
  FROM './export/properties.parquet' m
  JOIN './export/nodes.parquet' a
    ON m.path = a.path || '/jcr:content/metadata'
  LEFT JOIN './export/properties.parquet' o
    ON o.path = a.path || '/jcr:content/renditions/original/jcr:content'
    AND o.name = 'jcr:data'
  WHERE m.name = 'dc:format'
    AND a.primary_type = 'dam:Asset'
  GROUP BY 1
  ORDER BY assets DESC, 1;"
Content fragments by model

Count content fragments per cq:model, read from each fragment's jcr:content/data node.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT p.value AS model, count(*) AS fragments
  FROM './export/properties.parquet' p
  JOIN './export/nodes.parquet' a
    ON p.path = a.path || '/jcr:content/data'
  WHERE p.name = 'cq:model'
    AND a.primary_type = 'dam:Asset'
  GROUP BY 1
  ORDER BY fragments DESC, 1;"
Sling resource types by site

Rank sling:resourceType usage within each site; ties can return more than five types.

Link to example
DuckDB / Parquet
duckdb -c "
  WITH uses AS (
    SELECT
      regexp_extract(path, '^(/content/[^/]+)', 1) AS site,
      value AS resource_type,
      count(DISTINCT path) AS uses
    FROM './export/properties.parquet'
    WHERE name = 'sling:resourceType'
      AND path LIKE '/content/%'
    GROUP BY 1, 2
  )
  SELECT site, resource_type, uses, rank
  FROM (
    SELECT
      site,
      resource_type,
      uses,
      rank() OVER (PARTITION BY site ORDER BY uses DESC) AS rank
    FROM uses
  )
  WHERE rank <= 5
  ORDER BY site, rank;"
Duplicate content titles

Find repeated jcr:title values across distinct nodes; repeated titles are not necessarily errors.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT value AS title, count(DISTINCT path) AS uses
  FROM './export/properties.parquet'
  WHERE name = 'jcr:title'
    AND value IS NOT NULL
  GROUP BY 1
  HAVING count(DISTINCT path) > 1
  ORDER BY uses DESC;"
Folders with many direct children

Find parents with more than 200 immediate children; this is a structural inventory, not a performance diagnosis.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT parent_path, count(*) AS children
  FROM './export/nodes.parquet'
  WHERE parent_path IS NOT NULL
  GROUP BY 1
  HAVING count(*) > 200
  ORDER BY children DESC;"
Resource types used once

Find sling:resourceType values used by a single node; each result needs review before changing content.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT value AS resource_type, count(DISTINCT path) AS uses
  FROM './export/properties.parquet'
  WHERE name = 'sling:resourceType'
  GROUP BY 1
  HAVING count(DISTINCT path) = 1
  ORDER BY 1;"
Widely reused DAM assets

Count distinct nodes with fileReference values pointing into /content/dam; this covers that property only.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT value AS asset, count(DISTINCT path) AS refs
  FROM './export/properties.parquet'
  WHERE name = 'fileReference'
    AND value LIKE '/content/dam/%'
  GROUP BY 1
  HAVING count(DISTINCT path) >= 10
  ORDER BY refs DESC;"
Nodes with many properties

Count distinct property names, not property values; multivalued properties contribute one name.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT path, count(DISTINCT name) AS props
  FROM './export/properties.parquet'
  GROUP BY 1
  HAVING count(DISTINCT name) > 80
  ORDER BY props DESC
  LIMIT 50;"
Paths absent from the export

Review absolute path-shaped values with no exported node; a partial export or a string that is not a repository reference can produce a match.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT p.path, p.name, p.value
  FROM './export/properties.parquet' p
  WHERE p.value LIKE '/%'
    AND NOT EXISTS (
      SELECT 1 FROM './export/nodes.parquet' n
      WHERE n.path = p.value
    );"
AEM pages without jcr:content

Find cq:Page nodes whose jcr:content child is absent; use a full-depth export before treating a result as missing content.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT n.path
  FROM './export/nodes.parquet' n
  WHERE n.primary_type = 'cq:Page'
    AND NOT EXISTS (
      SELECT 1 FROM './export/nodes.parquet' c
      WHERE c.path = n.path || '/jcr:content'
    )
  ORDER BY 1;"
Recorded replication actions

Group cq:lastReplicationAction values by distinct node; historical metadata does not establish current publish state.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT value AS action, count(DISTINCT path) AS nodes
  FROM './export/properties.parquet'
  WHERE name = 'cq:lastReplicationAction'
  GROUP BY 1
  ORDER BY 2 DESC;"
Largest stored binaries

Rank non-null binary_length values; external binaries are represented by binary_reference rather than inline bytes.

Link to example
DuckDB / Parquet
duckdb -c "
  SELECT path, name, binary_length
  FROM './export/properties.parquet'
  WHERE binary_length IS NOT NULL
  ORDER BY binary_length DESC
  LIMIT 25;"

Parquet stores one row per node and one row per property value; SQLite exposes node_paths and properties_expanded. Completed Parquet exports carry matching revision stamps, but a query during file replacement can observe a mixed pair; see the export consistency contract.

Implementation & reference

Compatibility boundaries

Reads store.version=1 and 2; maintenance targets version 2, with conditional upgrades for version 1 cleanup.

Write-path interoperability is tested with Oak 1.90.0 in Apache Sling; AEM itself and external blob stores remain unverified.

Lucene index import and reindex are beta in v0.12.0; unsupported rebuild features are refused rather than approximated.

Interoperability test contract