<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Demetrius Albuquerque</title>
    <description>The latest articles on DEV Community by Demetrius Albuquerque (@maltzsama).</description>
    <link>https://dev.to/maltzsama</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3091218%2F2d4cb135-c70a-442b-a044-f7ace409d22c.png</url>
      <title>DEV Community: Demetrius Albuquerque</title>
      <link>https://dev.to/maltzsama</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maltzsama"/>
    <language>en</language>
    <item>
      <title>Sumeh: one API for data quality across 14 engines</title>
      <dc:creator>Demetrius Albuquerque</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/maltzsama/sumeh-one-api-for-data-quality-across-14-engines-5hm7</link>
      <guid>https://dev.to/maltzsama/sumeh-one-api-for-data-quality-across-14-engines-5hm7</guid>
      <description>&lt;p&gt;Every data quality framework eventually forces you into a choice. Great Expectations gives you a rich ecosystem, but you're managing suites, checkpoints, and a data context — it's a platform, not a library. Soda pushes you toward SodaCL and SodaCloud, with the open-source layer getting thinner every release. Pandera is excellent for schema and type validation, but stops there — no aggregation rules, no SQL engines, no row-level tagging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sumeh&lt;/strong&gt; takes a different position: it's a library, not a platform. It does one thing — run validation rules against data — and does it across every major engine with the same API and the same return type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sumeh&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sumeh.core.rules.rule_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RuleDefinition&lt;/span&gt;

&lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;RuleDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;RuleDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_between&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;good_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pass rate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pass_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rules, same &lt;code&gt;ValidationReport&lt;/code&gt;, whether the engine underneath is Pandas, Polars, PySpark, Dask, DuckDB, BigQuery, Snowflake, Athena, PyFlink, or Ray.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm actually proud of: single-pass bifurcation
&lt;/h2&gt;

&lt;p&gt;Most validation tools check your data and hand you a report. Sumeh also splits the dataset into clean rows and quarantine rows — in the same scan that computes the metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;good_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# good_df — original columns, clean
# bad_df  — original columns + _dq_errors (which rules failed, per row)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No double scanning, no extra joins. And on PySpark specifically, this matters more than it sounds: the bifurcation is built entirely from &lt;code&gt;Column&lt;/code&gt; expressions evaluated lazily across the cluster. &lt;code&gt;.collect()&lt;/code&gt; is never called, not even for sampling. That's a real limitation in libraries like Deequ — pulling a full dataset to the driver for row-tagging is a driver OOM waiting to happen once you're past toy data sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v3.0 changed
&lt;/h2&gt;

&lt;p&gt;The big architectural break already happened in v2.0 — a full rewrite: namespace-first API (&lt;code&gt;from sumeh import pandas; pandas.validate(...)&lt;/code&gt;), a &lt;code&gt;ValidationReport&lt;/code&gt; object instead of a bare tuple, SQL generation rebuilt on SQLGlot's AST instead of string concatenation, and the &lt;code&gt;cuallee&lt;/code&gt; dependency dropped entirely.&lt;/p&gt;

&lt;p&gt;v3.0, released this month, builds on that same foundation — added functionality, no API breaks. If you're on v2.x, upgrading should be a non-event.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;p&gt;I want this comparison to be accurate rather than flattering, so here's what I found checking each project's current docs rather than trusting my own assumptions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sumeh&lt;/th&gt;
&lt;th&gt;Great Expectations&lt;/th&gt;
&lt;th&gt;Soda Core&lt;/th&gt;
&lt;th&gt;pandera&lt;/th&gt;
&lt;th&gt;cuallee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engines / backends&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;3 execution engines (Pandas, Spark, SQLAlchemy — the latter covers ~9 SQL dialects)&lt;/td&gt;
&lt;td&gt;~10 data sources (Postgres, Snowflake, BigQuery, Databricks, Redshift, and others)&lt;/td&gt;
&lt;td&gt;4 native backends (pandas, pyspark, polars, ibis), plus dask/modin via the pandas backend&lt;/td&gt;
&lt;td&gt;~7 (PySpark, Pandas, Snowpark, Polars, DuckDB, BigQuery, Daft)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row-level split of good/bad data&lt;/td&gt;
&lt;td&gt;Native, one method call (&lt;code&gt;report.split()&lt;/code&gt;), same pass as validation&lt;/td&gt;
&lt;td&gt;Possible — &lt;code&gt;unexpected_index_list&lt;/code&gt;/&lt;code&gt;unexpected_list&lt;/code&gt; expose failing rows, but it's a manual extraction step, not a built-in split&lt;/td&gt;
&lt;td&gt;Surfaces "bad" rows per check, but not a single clean/quarantine split call&lt;/td&gt;
&lt;td&gt;Has &lt;code&gt;drop_invalid_rows&lt;/code&gt; to remove bad rows, no built-in quarantine output&lt;/td&gt;
&lt;td&gt;No built-in split&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL generation approach&lt;/td&gt;
&lt;td&gt;SQLGlot AST, compiled per dialect at call time&lt;/td&gt;
&lt;td&gt;Compiles through SQLAlchemy per dialect&lt;/td&gt;
&lt;td&gt;Compiles SodaCL checks to SQL&lt;/td&gt;
&lt;td&gt;N/A (DataFrame-native, not SQL-based)&lt;/td&gt;
&lt;td&gt;N/A (DataFrame-native, not SQL-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata/catalog export&lt;/td&gt;
&lt;td&gt;Zero-SDK payload generation for OpenMetadata — you own the HTTP calls&lt;/td&gt;
&lt;td&gt;Official first-party OpenMetadata integration via &lt;code&gt;OpenMetadataValidationAction&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Integrates with Soda Cloud (hosted) for monitoring/lineage&lt;/td&gt;
&lt;td&gt;None built-in&lt;/td&gt;
&lt;td&gt;None built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PySpark bifurcation avoids &lt;code&gt;.collect()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;fail_condition&lt;/code&gt; Column expressions, evaluated lazily&lt;/td&gt;
&lt;td&gt;N/A (bifurcation isn't a first-class feature)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Uses PySpark's Observation API, not &lt;code&gt;.collect()&lt;/code&gt;, for metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pip install&lt;/code&gt; and go, no mandatory config files&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — requires a Data Context&lt;/td&gt;
&lt;td&gt;Yes, YAML-based&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few honest caveats on top of the table: Great Expectations' OpenMetadata support is more mature than Sumeh's, just built differently — official SDK-backed action vs. Sumeh's dependency-free dict generator that you POST yourself. And cuallee deserves a specific callout — its clean API is what inspired Sumeh's design in the first place. Sumeh extends that idea to more engines, layers SQL generation via SQLGlot on top, and adds a profiler alongside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is actually at
&lt;/h2&gt;

&lt;p&gt;Sumeh is not production-ready — that's the honest answer, and it's the reason for this post. It's early: a handful of engines have solid test coverage (Pandas, PySpark, DuckDB), others are newer and less battle-tested. This is exactly the stage where outside testing matters most, before assumptions calcify.&lt;/p&gt;

&lt;p&gt;Concretely, here's where more eyes would help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ray Data and PyFlink&lt;/strong&gt; — these engines have thinner test coverage than the core batch engines. If you already use either in your stack, running Sumeh against real workloads there is high-value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL dialect edge cases&lt;/strong&gt; — every SQL engine shares a compiler that builds queries as SQLGlot AST and compiles to the target dialect at call time. Snowflake, Trino, Doris, Athena, Redshift — each has its own quirks. If you run any of these in production, generating validation SQL against your actual schema (&lt;code&gt;sumeh sql rules.csv --table your.table --dialect snowflake&lt;/code&gt;) and checking the output would catch a lot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bifurcation at real scale&lt;/strong&gt; — tested so far on dev-sized data, not production volume. If you can point it at something large and report back on memory behavior, timing, and correctness, that's exactly the kind of feedback this needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date and schema validation rules&lt;/strong&gt; — the newest and least-covered rule categories.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;sumeh                &lt;span class="c"&gt;# Pandas included by default&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;sumeh[pyspark]       &lt;span class="c"&gt;# or polars, duckdb, bigquery, snowflake, ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/maltzsama/sumeh" rel="noopener noreferrer"&gt;github.com/maltzsama/sumeh&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://maltzsama.github.io/sumeh/" rel="noopener noreferrer"&gt;maltzsama.github.io/sumeh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To contribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/maltzsama/sumeh.git
&lt;span class="nb"&gt;cd &lt;/span&gt;sumeh
git checkout develop
poetry &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--with&lt;/span&gt; dev
poetry run pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Issues, PRs, and bug reports are genuinely welcome — especially in the areas above. If something breaks in your setup, open an issue with the traceback; it'll get read and acted on.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>dataengineering</category>
      <category>pyspark</category>
    </item>
    <item>
      <title>NyaruDB2: an embedded document database for Swift, built on Codable and actors</title>
      <dc:creator>Demetrius Albuquerque</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:30:00 +0000</pubDate>
      <link>https://dev.to/maltzsama/nyarudb2-an-embedded-document-database-for-swift-built-on-codable-and-actors-ode</link>
      <guid>https://dev.to/maltzsama/nyarudb2-an-embedded-document-database-for-swift-built-on-codable-and-actors-ode</guid>
      <description>&lt;p&gt;Every iOS developer who has shipped a Core Data app knows the ritual. You open the visual model editor. You define an entity. You generate a class. You inherit from &lt;code&gt;NSManagedObject&lt;/code&gt;. You learn about contexts, and then about the difference between the view context and a private context, and then about the merge policy you have to set because two contexts disagreed. You write a lightweight migration. It fails on a device you don't have.&lt;/p&gt;

&lt;p&gt;Meanwhile, the thing you actually wanted to persist was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Codable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NyaruDB2&lt;/strong&gt; is an embedded document database that lets you persist exactly that. No base class, no schema file, no code generation, no migration ceremony. Your &lt;code&gt;Codable&lt;/code&gt; structs go in, your &lt;code&gt;Codable&lt;/code&gt; structs come out, and everything in between is &lt;code&gt;async/await&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;NyaruDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CollectionOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;idField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;indexedFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"u1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Ana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"São Paulo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;adults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isEqualTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"São Paulo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isGreaterThanOrEqualTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// → [User]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole API surface for the common case. What I want to talk about in this post is what's underneath it — because "just use Codable" is easy to say and considerably harder to make fast and safe.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Status:&lt;/strong&gt; NyaruDB2 is experimental. It works, it's tested, and it is not yet running in anyone's production app — including mine. I'm publishing the architecture because the design decisions are the interesting part, and because I'd rather have them torn apart in public than defend them alone.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The layers
&lt;/h2&gt;

&lt;p&gt;The stack is four pieces, each with one job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NyaruDB&lt;/code&gt;&lt;/strong&gt; — the database handle. An actor that owns your collections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NyaruCollection&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; — the &lt;em&gt;only&lt;/em&gt; layer that knows what &lt;code&gt;T&lt;/code&gt; is. It encodes your struct to bytes going in, and decodes bytes back into your struct coming out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;QueryBuilder&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; — fluent predicates, sort, limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CollectionCore&lt;/code&gt;&lt;/strong&gt; — the engine underneath all three: an actor that owns the indexes, routes documents to shard files, and gates compaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's one invariant here that shapes everything else: &lt;strong&gt;type erasure happens at the collection boundary.&lt;/strong&gt; Below &lt;code&gt;NyaruCollection&amp;lt;T&amp;gt;&lt;/code&gt;, nothing knows about &lt;code&gt;User&lt;/code&gt;. The engine works on &lt;code&gt;Data&lt;/code&gt;, and when it needs a single field out of a document — an id, an index key — it pulls that one field out of the encoded bytes without decoding the whole document.&lt;/p&gt;

&lt;p&gt;That's why there's no base class. The engine doesn't need one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Storage: slots, not an append log
&lt;/h2&gt;

&lt;p&gt;A collection's data lives in &lt;strong&gt;shard files&lt;/strong&gt;. Each shard is a flat sequence of slots, and each slot holds one record: a 16-byte header plus a payload.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bytes reserved for the payload — &lt;strong&gt;rounded up to a multiple of 32&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payloadLength&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bytes actually used (&lt;code&gt;≤ capacity&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bit 0 = tombstone; bits 1–3 = compression method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reserved&lt;/td&gt;
&lt;td&gt;3 B&lt;/td&gt;
&lt;td&gt;keeps the header 16-byte aligned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;crc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CRC32 of the payload as stored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap between &lt;code&gt;payloadLength&lt;/code&gt; and &lt;code&gt;capacity&lt;/code&gt; is slack, and the slack is the entire trick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4sb06ap41xz6l1x9yr1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4sb06ap41xz6l1x9yr1r.png" alt="slots" width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An update is usually a single &lt;code&gt;pwrite&lt;/code&gt; at the same offset.&lt;/strong&gt; If the new version of the document fits the capacity already reserved — which the 32-byte padding is designed to make likely — the record doesn't move. The index pointer doesn't change. Nothing else has to be touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A delete is one byte.&lt;/strong&gt; Set the tombstone bit in the flags, in place. The slot's &lt;code&gt;(offset, capacity)&lt;/code&gt; then joins an in-memory free list kept sorted by capacity, so the next insert can binary-search it for a &lt;strong&gt;best fit&lt;/strong&gt; and reuse the space.&lt;/p&gt;

&lt;p&gt;Before reusing a free slot, the engine re-reads the header from disk and verifies that the capacity matches and the tombstone bit is genuinely set. If the disk disagrees with the free list, &lt;strong&gt;the slot is forgotten rather than overwritten.&lt;/strong&gt; A stale entry in an in-memory structure can never clobber a live record on disk. This pattern — when in doubt, do nothing dangerous — shows up all over the engine, and I'll come back to it.&lt;/p&gt;

&lt;p&gt;Reads are direct: a point read is one &lt;em&gt;speculative&lt;/em&gt; &lt;code&gt;pread&lt;/code&gt; of up to 4 KiB from the record's offset, so the header and (for typical documents) the entire payload arrive in a &lt;strong&gt;single syscall&lt;/strong&gt;. Full scans stream the file through a 4 MiB sliding window, which means scanning a 2 GB collection uses 4 MB of memory, not 2 GB.&lt;/p&gt;




&lt;h2&gt;
  
  
  Partitioning: your data's natural shape becomes the file layout
&lt;/h2&gt;

&lt;p&gt;Declare a &lt;code&gt;partitionKey&lt;/code&gt;, and documents route to shard files by that field's value. A document with &lt;code&gt;city: "São Paulo"&lt;/code&gt; and no encryption configured lands in &lt;code&gt;SaoPaulo.nyaru&lt;/code&gt;. With an encryption key set, the filename is instead &lt;code&gt;HMAC-SHA256(value, key)&lt;/code&gt; — something like &lt;code&gt;a3f9…c1.nyaru&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things fall out of this.&lt;/p&gt;

&lt;p&gt;First, a query filtered on the partition key reads &lt;strong&gt;exactly one file&lt;/strong&gt; and ignores every other. Partitioning isn't a logical grouping — it's physical.&lt;/p&gt;

&lt;p&gt;Second, notice the encryption branch. If your partition key is something like customer name or user id, then unencrypted shard filenames would leak those values through the filesystem even though the record contents are sealed. So when encryption is on, the filename is an HMAC of the partition value. The directory listing tells an attacker nothing.&lt;/p&gt;

&lt;p&gt;(The corollary: partition on something with tens or hundreds of distinct values, not on the document id. One file per document is a misuse, and the engine won't stop you.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Indexes: sorted arrays, and a trick for deletes
&lt;/h2&gt;

&lt;p&gt;Every indexed field gets an &lt;code&gt;OrderedIndex&lt;/code&gt;, held &lt;strong&gt;entirely in memory&lt;/strong&gt; and persisted as a snapshot. It's two parallel sorted arrays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keys:     [ "Belo Horizonte", "Curitiba",  "Rio",  "São Paulo" ]
postings: [ [p1, p7],         [],          [p3],   [p2, p4, p9] ]
                              ↑ dead slot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lookups binary-search &lt;code&gt;keys&lt;/code&gt;; the matching &lt;code&gt;postings&lt;/code&gt; entry gives you the record pointers.&lt;/p&gt;

&lt;p&gt;The interesting part is that empty slot. Removing a key from a sorted array shifts every element behind it — O(n) per removal. And it's worse than it sounds: deleting documents one by one tends to empty the corresponding key in the id index &lt;em&gt;every time&lt;/em&gt;, and a FIFO deletion pattern empties position 0 repeatedly. That's the pathological case, hit over and over.&lt;/p&gt;

&lt;p&gt;So when a posting list becomes empty, &lt;strong&gt;the key stays in the array as a dead slot&lt;/strong&gt; — semantically absent, physically present — and the arrays get rebuilt in a single O(n) sweep only once dead slots exceed a quarter of the keys. Removal becomes amortised O(1).&lt;/p&gt;

&lt;p&gt;The invariant every reader honours: &lt;code&gt;postings[i].isEmpty&lt;/code&gt; means &lt;em&gt;the key is absent&lt;/em&gt;. &lt;code&gt;search&lt;/code&gt; and &lt;code&gt;range&lt;/code&gt; are naturally correct (an empty list contributes nothing). &lt;code&gt;contains&lt;/code&gt; and &lt;code&gt;allKeys&lt;/code&gt; check explicitly. Bulk rebuilds drop dead slots for free, since they're rebuilding the arrays anyway.&lt;/p&gt;




&lt;h2&gt;
  
  
  Queries: the index answers, or it narrows
&lt;/h2&gt;

&lt;p&gt;The planner runs &lt;strong&gt;once&lt;/strong&gt; per execution. It flattens the top-level &lt;code&gt;AND&lt;/code&gt; conjuncts, asks the engine in a &lt;strong&gt;single actor hop&lt;/strong&gt; which of the referenced fields are indexed, and picks a strategy — in priority order: an index lookup on an indexed field (equality beats a set-membership check beats a range), then a single-file partition scan if the partition key has an equality predicate, then a full scan as the fallback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckuo61yb9tbiuva2ffa9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckuo61yb9tbiuva2ffa9.png" alt="query plan" width="732" height="1041"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Covered queries are the fast lane.&lt;/strong&gt; A query counts as covered when exactly one top-level predicate exists and the index alone can answer it — no sort, or a sort on that same indexed field, ascending. Because the index alone answers the predicate, &lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;offset&lt;/code&gt; are pushed &lt;em&gt;into&lt;/em&gt; the index range scan — a &lt;code&gt;.limit(10)&lt;/code&gt; over a range matching 100,000 documents stops collecting pointers after 10. It never reads the other 99,990.&lt;/p&gt;

&lt;p&gt;Two more paths never touch the disk at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;count()&lt;/code&gt; on a covered query counts pointers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;distinctValues(on:)&lt;/code&gt; over an indexed field enumerates the index keys directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when records &lt;em&gt;are&lt;/em&gt; read, the pointers get &lt;strong&gt;sorted by offset first&lt;/strong&gt;. Index order is key order, which is random file order; sorting turns random seeks into a forward walk of the file. Sorted-and-limited queries run through a bounded top-K heap, so &lt;code&gt;.sort(...).limit(10)&lt;/code&gt; over a huge candidate set keeps 10 items in memory, not all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concurrency: actors, not locks
&lt;/h2&gt;

&lt;p&gt;There's one &lt;code&gt;ShardActor&lt;/code&gt; per shard file, so writes to different partitions proceed genuinely in parallel and never contend. &lt;code&gt;CollectionCore&lt;/code&gt; is a single actor because the indexes are shared mutable state — it's the serialization point for index maintenance, and the compiler enforces that, not a code review comment.&lt;/p&gt;

&lt;p&gt;Crucially, &lt;strong&gt;CPU-bound work is pushed outside the actors&lt;/strong&gt;. Encoding, CRC, compression, encryption, and decoding are pure functions over &lt;code&gt;Data&lt;/code&gt;. They run across all cores through a parallel map while no actor is held. The actor protects the &lt;em&gt;state&lt;/em&gt;; it doesn't get to hog the &lt;em&gt;CPU&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The compaction gate
&lt;/h3&gt;

&lt;p&gt;Compaction rewrites a shard file — which invalidates every offset stored in it — and &lt;em&gt;then&lt;/em&gt; remaps the index pointers to the new offsets. The actor suspends at each &lt;code&gt;await&lt;/code&gt; inside that operation, and that's a window. A concurrent &lt;code&gt;get&lt;/code&gt; could read a stale offset against the already-rewritten file. A concurrent &lt;code&gt;insert&lt;/code&gt; could register a pointer into the new file that the subsequent remap then discards as "stale."&lt;/p&gt;

&lt;p&gt;Both are silent-wrong-answer bugs, which are the worst kind.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foknxobte9bxgukgps71n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foknxobte9bxgukgps71n.png" alt="actors and locks" width="789" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So every pointer-based operation brackets itself with &lt;code&gt;beginPointerOp()&lt;/code&gt; / &lt;code&gt;endPointerOp()&lt;/code&gt;, and compaction drains them all before touching a file. Scans don't take the gate — they walk the file for live records rather than following stored pointers, so a rewrite can't strand them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Durability: the dirty flag, and what happens after a crash
&lt;/h2&gt;

&lt;p&gt;Each shard file carries a dirty bit in its header.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9hd0yrra45rxxplz85x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9hd0yrra45rxxplz85x.png" alt="dirty flag" width="799" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ordering is the whole design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setting&lt;/strong&gt; the flag is fsynced &lt;em&gt;before&lt;/em&gt; the first write lands. A crash therefore can't produce modified data behind a clean flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clearing&lt;/strong&gt; it writes a state sidecar first (the free list and live count, so the next clean open can skip a full scan), &lt;em&gt;then&lt;/em&gt; clears the flag. A crash in between leaves the flag set — which forces a full recovery scan. Expensive, but never wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sidecar, by the way, is validated on load against a magic number, a version, the actual file size, and a CRC of its own contents. Any mismatch and it's ignored in favour of a scan. &lt;strong&gt;It's a cache, never an authority.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On recovery, the engine verifies everything: every live record's CRC is recomputed, and a record that fails is tombstoned (a corrupt record that is &lt;em&gt;readable&lt;/em&gt; is more dangerous than one that's gone). An invalid header mid-file means a torn append, and the file is truncated there. Index snapshots from before the crash are discarded wholesale and rebuilt from the surviving data — offsets recorded before a crash can't be trusted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fsync&lt;/code&gt; itself is a policy, not a hardcoded rule: &lt;code&gt;.off&lt;/code&gt; by default (fsync on explicit &lt;code&gt;sync()&lt;/code&gt; and &lt;code&gt;close()&lt;/code&gt;), or &lt;code&gt;.afterWrites(n)&lt;/code&gt;, or &lt;code&gt;.interval(seconds)&lt;/code&gt;. On mobile, "always fsync" is a battery decision as much as a safety one, and it should be yours to make.&lt;/p&gt;




&lt;h2&gt;
  
  
  Atomic batches, and a bias against losing data
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;writeBatch&lt;/code&gt; runs mixed inserts/updates/upserts/deletes in four phases, strictly in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate &amp;amp; resolve&lt;/strong&gt; — reads only: duplicate checks, resolving existing pointers, extracting old index keys. Any throw here leaves the collection completely untouched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append new versions&lt;/strong&gt; — one batched write per shard. Old versions are still intact on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tombstone old versions&lt;/strong&gt; — updates, upserts, deletes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update indexes&lt;/strong&gt; — pure in-memory. Cannot fail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hard case is a failure &lt;em&gt;during&lt;/em&gt; phase 3, where some old versions got tombstoned and others didn't. You can't roll that back — the tombstone already landed on disk. So, per operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old version already tombstoned?&lt;/strong&gt; The newly appended version is now the &lt;em&gt;only&lt;/em&gt; copy of that document. Rolling it back would &lt;strong&gt;destroy data&lt;/strong&gt;. Keep it, and make it visible in the indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old version still live, or it was a plain insert?&lt;/strong&gt; Tombstone the new append. The pre-batch state stays authoritative.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if probing the old record fails, the engine &lt;em&gt;assumes it was tombstoned&lt;/em&gt; — because preferring the new version risks a duplicate after an index rebuild, while wrongly assuming "still live" would tombstone the only surviving copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicates are recoverable. Data loss is not.&lt;/strong&gt; That asymmetry is why the phases are ordered the way they are, and it's the same instinct as the free-slot check earlier: when the engine can't prove which of two states it's in, it picks the one you can come back from.&lt;/p&gt;




&lt;h2&gt;
  
  
  What NyaruDB2 deliberately doesn't do
&lt;/h2&gt;

&lt;p&gt;I think a design is best judged by what it refuses.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Not doing&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A full LSM-tree&lt;/strong&gt; (SSTables, leveled compaction, bloom filters)&lt;/td&gt;
&lt;td&gt;The engine already has the useful subset: batched sequential appends, an in-memory sorted index, tombstones, streaming compaction. The rest of the LSM machinery buys sustained write throughput at the price of background write amplification — which on a phone is battery drain and flash wear.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Faulting / lazy proxy objects&lt;/strong&gt; (the Core Data trick)&lt;/td&gt;
&lt;td&gt;It requires intercepting property access, which the Obj-C runtime gives &lt;code&gt;NSManagedObject&lt;/code&gt; and Swift does not give a &lt;code&gt;Codable&lt;/code&gt; struct. Doing it means giving up the plain-struct API — the one thing this project won't trade. A projection API gets most of the benefit without it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;mmap&lt;/code&gt; on the read path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Point reads are already a single &lt;code&gt;pread&lt;/code&gt;. &lt;code&gt;mmap&lt;/code&gt; fights iOS file protection (page faults after the device locks), needs remapping when the file grows, and needs a fence around compaction's file swap or a live pointer becomes a use-after-unmap crash. And it can't help at all when payloads are compressed or encrypted, because the bytes on disk aren't the bytes you want.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A background compaction thread&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Predictability. &lt;code&gt;needsCompaction()&lt;/code&gt; tells you when it's worth doing; &lt;code&gt;compact()&lt;/code&gt; does it when &lt;em&gt;you&lt;/em&gt; say so. Nothing spends your users' battery behind your back.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;It's a document store. The query builder is typed, composable, and survives a rename refactor. A SQL string is none of those.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;It's real code: encryption at rest (AES-GCM per record, sealed manifests, sealed index snapshots), compression (gzip/lzfse/lz4), file protection, crash recovery, atomic batches, streaming cursors. It has a test suite. It runs on macOS and iOS.&lt;/p&gt;

&lt;p&gt;It is also &lt;strong&gt;experimental, pre-1.0, and has no production users.&lt;/strong&gt; I'm still working on performance — the query path in particular has a real gap against Core Data that I'm chasing, and I'll write about that separately, along with the story of how I discovered that most of my &lt;em&gt;other&lt;/em&gt; apparent gaps were bugs in my own benchmark harness rather than in the engine.&lt;/p&gt;

&lt;p&gt;Issues, disagreements, and "you're wrong about mmap and here's why" are all welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/maltzsama/nyarudb2" rel="noopener noreferrer"&gt;github.com/maltzsama/nyarudb2&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>NyaruDB2: A Mobile-First Experiment in Table Partitioning for iOS</title>
      <dc:creator>Demetrius Albuquerque</dc:creator>
      <pubDate>Fri, 25 Apr 2025 23:52:07 +0000</pubDate>
      <link>https://dev.to/maltzsama/nyarudb2-a-mobile-first-experiment-in-table-partitioning-for-ios-anl</link>
      <guid>https://dev.to/maltzsama/nyarudb2-a-mobile-first-experiment-in-table-partitioning-for-ios-anl</guid>
      <description>&lt;h1&gt;
  
  
  NyaruDB2
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What is NyaruDB2?
&lt;/h2&gt;

&lt;p&gt;NyaruDB2 is an experimental, embeddable database library for iOS &amp;amp; macOS, written in Swift. Its primary goal is to explore &lt;strong&gt;table partitioning&lt;/strong&gt; and &lt;strong&gt;indexed queries&lt;/strong&gt; in a mobile context—think of lightweight shards instead of one monolithic file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Disclaimer:&lt;/strong&gt; This is very much a proof-of-concept. APIs may change, and it’s not intended for production use… yet!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key Concepts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partitioning (Sharding):&lt;/strong&gt; Split collections into multiple files (“shards”) based on a key (e.g. a &lt;code&gt;category&lt;/code&gt; field), reducing I/O for targeted queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexes:&lt;/strong&gt; Built-in B-Tree indexes accelerate equality, range, prefix and substring searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression:&lt;/strong&gt; Supports multiple algorithms (none, GZIP, LZFSE, LZ4) to trade off size vs. CPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Core Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Actor-Backed B-Tree Indexes Provide equality and range queries (equal, greaterThan, between, startsWith, contains) using a thread-safe, in-memory B-Tree implementation.&lt;/li&gt;
&lt;li&gt;Async/Await API Non-blocking insert, fetch, update, and delete operations with a fluent query builder.&lt;/li&gt;
&lt;li&gt;Pluggable Compression Choose between gzip, LZFSE, or LZ4 on a per-shard basis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Add NyaruDB2 to your &lt;code&gt;Package.swift&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// swift-tools-version:5.9&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;PackageDescription&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"YourApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/galileostudio/nyarudb2.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upToNextMinor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"0.1.0-alpha1"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nv"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"YourApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NyaruDB2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"nyarudb2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;swift package update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;NyaruDB2&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Initialize&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;NyaruDB2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"AppData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;shardKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// enable partitioning on “category”&lt;/span&gt;
  &lt;span class="nv"&gt;compressionMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// compress shards with GZIP&lt;/span&gt;
  &lt;span class="nv"&gt;fileProtectionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completeUntilFirstUserAuthentication&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Define your model&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Codable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Equatable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Insert documents&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="kt"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Fruit"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="kt"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Carrot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Vegetable"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;into&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Inventory"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Query with auto-shard pruning&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Inventory"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fruit"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;fruits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fruits:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Learn More &amp;amp; Contribute
&lt;/h2&gt;

&lt;p&gt;🔗 Repo: &lt;a href="https://github.com/galileostudio/nyarudb2" rel="noopener noreferrer"&gt;https://github.com/galileostudio/nyarudb2&lt;/a&gt;&lt;br&gt;&lt;br&gt;
📄 Docs: &lt;a href="https://galileostudio.github.io/nyarudb2/" rel="noopener noreferrer"&gt;https://galileostudio.github.io/nyarudb2/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This project is an open playground for database ideas on mobile. Feedback, stars ⭐, issues and PRs are very welcome!&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/maltzsama" rel="noopener noreferrer"&gt;Demetrius Albuquerque&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>database</category>
      <category>ios</category>
      <category>mobile</category>
      <category>swift</category>
    </item>
  </channel>
</rss>
