Metadata-Version: 2.4
Name: readb
Version: 0.1.0
Summary: A transparent, read-only SQL query layer over Open Knowledge Format (OKF) bundles.
Project-URL: Homepage, https://github.com/slukyano/readb
Project-URL: Repository, https://github.com/slukyano/readb
Author-email: Stanislav Lukyanov <stan@slukyano.dev>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: duckdb,frontmatter,knowledge,markdown,okf,sql
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database
Requires-Python: >=3.11
Requires-Dist: click>=8.1
Requires-Dist: duckdb>=1.1
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# readb

A transparent, read-only SQL query layer over an **Open Knowledge Format (OKF)** bundle — a
directory of markdown files with YAML frontmatter — so an agent or a human can run real SQL
against the wiki with no explicit database-creation step.

readb loads a bundle into an embedded [DuckDB](https://duckdb.org/) engine and lets DuckDB
execute the SQL. There is no custom SQL parser or query planner, and the source files are never
modified.

> Status: MVP implemented. Bundle loading, type inference, the library API, and the CLI all
> work; the 12 acceptance criteria in [`docs/design-brief.md`](docs/design-brief.md) are covered
> by tests. Not yet built (left as clean seams): persistent on-disk index, git-aware incremental
> rebuild, and write-back — all explicit non-goals for this MVP.

## Install

```sh
uv sync
```

## Usage

Library:

```python
import readb

db = readb.open("./path/to/bundle")          # builds an in-memory DuckDB; no files written
rows = db.sql("SELECT * FROM __DOCUMENTS WHERE type = 'Metric'")
```

CLI:

```sh
readb query "SELECT * FROM __DOCUMENTS" --bundle ./path       # results as a table
readb query "SELECT * FROM __DOCUMENTS" --bundle ./path --format json   # or csv | tsv | raw
readb schema --bundle ./path                                  # detected types, tables, columns
readb show --bundle ./path some-concept                       # a concept's body, by name
```

Concepts are addressed wiki-style: a simple file name (`some-concept`) when it is unique in
the bundle, or the full path (`sub/some-concept.md`) — always unambiguous — when it is not.
An ambiguous bare name is always a hard error listing the clashing paths — never a silent
first match. Names are filenames: `__name` is immutable and filename-derived, and a producer
`name:` frontmatter key is just an ordinary data column — it affects neither `__name` nor
addressing.

## readb init: skip --bundle

`--bundle` may be omitted once you register your bundles (the git model — a directory is a
bundle because you said so once):

```sh
readb init tasks docs/adr    # in the project root: writes .readb/config.toml (commit it)
cd tasks && readb query "SELECT count(*) FROM task"   # bundle resolved by walking up
```

Commands without `--bundle` walk up from the cwd to the nearest `.readb/` registry, then pick
the declared bundle containing the cwd (innermost wins), else the sole declared bundle, else
the config's optional `default_bundle`. Genuine ambiguity — e.g. the root of a repo declaring
several bundles, no default — is a hard error listing the choices, never a guess; a directory
never silently becomes a bundle. Explicit `--bundle <dir>` always works on any directory,
initialized or not, and never consults the registry. Re-running `init` merges new dirs and
never removes. Besides the config, `.readb/` is reserved for readb's future persistent index —
it is never read as bundle content.
readb only addresses files inside the bundle root: a `../` path or a symlink that resolves
outside the bundle is not supported and is refused with an explicit error (by name and by
path alike). `--format raw` prints values verbatim, so
`readb query "SELECT __raw FROM task WHERE __name = 'x'" --format raw` is exactly `cat`.

## Tables and views

| Name | Rows |
| --- | --- |
| `__DOCUMENTS` | one per concept (the six reserved OKF fields + the virtual fields below) |
| *per-type tables* | one per detected `type`; columns are reserved fields + the union of producer keys across docs of that type |
| `__INDEXES` | one per reserved `index.md` file; columns are the union of their frontmatter fields |
| `__LOG` | one per reserved `log.md` file (created only if any exist) |
| `__UNKNOWNTYPE` | one per non-conformant concept (no / non-string / empty-normalized `type`) |
| `__TAGS` | normalized `(concept_path, tag)` view for join-style tag filtering |

Every table also carries four virtual columns: `__path` (bundle-relative path, with `.md` —
the guaranteed-unique key), `__name` (the simple file name, no directories or `.md`; wiki-style,
assumed unique but not guaranteed), `__body` (the markdown body, frontmatter stripped), and
`__raw` (the byte-exact file text as on disk, frontmatter included).

## Type inference

Each column's type is inferred once at load time as the narrowest DuckDB type that losslessly
holds every observed value (`int`+`float` → `DOUBLE`; a scalar alongside a list → a `LIST`;
maps with a consistent key set → a `STRUCT`). Anything that doesn't reduce to a single engine
type is stored as a `JSON` column — nothing is dropped, and producer intent is never guessed
(strings are never split or parsed). The reserved `tags` field is always a `LIST`. Run
`readb schema` to see the inferred type of every column.

## Development

```sh
uv sync              # install deps + dev tools
uv run pytest        # run tests
uv run ruff check    # lint
uv run ruff format   # format
```

## License

[Apache 2.0](LICENSE) © 2026 Stanislav Lukyanov
