Postgres の LATERAL ジョインは、かなり優れた eDSL(埋め込みドメイン特定言語)の実現を可能にします。

2026/04/27 23:07

Postgres の LATERAL ジョインは、かなり優れた eDSL(埋め込みドメイン特定言語)の実現を可能にします。

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

rust-rel8
の紹介。PostgreSQL の lateral joins を活用し、型安全で合成可能なデータベースクエリを可能にする新しい Rust ライブラリです。2026-03-15 に公開されたこのプロジェクトは、lateral joins が FROM 節に先行する列をサブクエリで使用することを許容し、標準的な join を表現豊かな「CROSS JOIN LATERAL ... WHERE」形式に変換しつつ、従来のアプローチと同じクエリ計画を維持することを示しています。著者は、lateral joins が多くの ORM やクエリビルダーに欠けている表現力と合成可能性を提供すると主張しており、特にクエリの統合や多対多の関係性を扱い、かさばるマクロの魔法あるいは深くネストしたジェネリクスを必要としない点に価値を見出しています。
sqlx
などのツールは「generate bindings」によって型安全性を提供しますが、それらは依然として合成性は低いものです。一方、
rust-rel8
は eDSL アプローチを採用し、「表現力豊か」「合成可能」「型安全」「常に有効な SQL を生成する」という 4 つの目標を達成するとともに、ユーザー定義タイプと集計機能をサポートします。Haskell の Rel8 に着想を得て、Rust でその人体工学的クエリ構築パターンを再現しており、ユーザーごとに投稿を取得し、エンティティを組み合わせて集約結果を集約する、
.optional()
を通じてオプションな join、カスタムテーブル宣言、集計ビルダーなどを実装しています。実行時オーバーヘッドなしにコンパイル時にスコープルールを強制するために、ライブラリは
Table
ForLifetimeTable
ShortenLifetime
TableHKT
MapTable
などのコア特性によって管理される
'scope
というスコープ付きライフタイムを使用しています。これらの特性は、列を穿通し、クエリ構築後にライフタイムを入れ替え、モード(NameMode、ValueMode、ExprMode、EmptyMode)間でのフィールドタイプのマッピングを実現します。これにより、
sea_query
から取得されたクエリ AST が
Expr
タイプでラップされ、有効な SQL 生成が保証されます。

本文

PostgreSQL の Later Join を活用した高機能な eDSL

公開日: 2026 年 3 月 15 日

Later Join(ラテラル JOIN)は非常に巧妙であり、それを用いてクエリの関数型プログラムライブラリ(eDSL: expression-driven programming language)を構築できます。

Later Join の背景

PostgreSQL(そして少数の他のデータベース)には、あまり知られていないか利用されていないという種類の JOIN、すなわち

LATERAL
JOIN があります。これらは、結合するサブクエリ内で前述の
FROM
クロージーズから参照されるカラムを使用することを可能にします。

通常のクエリと Later Join

例として、以下の標準的なテーブル結合クエリを考えてみましょう。

SELECT *
FROM users u
INNER JOIN posts p ON u.id = p.user_id

これを Later Join で以下のように書き換えることができます。

SELECT *
FROM users u
CROSS JOIN LATERAL (
    SELECT * FROM posts p WHERE u.id = p.user_id
) p2

結合の種類が

INNER JOIN
から
CROSS
へと変わっているのがお分かりでしょう。通常、これはデカルト積(カトリアン・プロダクト)を意味するはずですが、サブクエリ内のフィルタ条件があるため、各投稿は引き続きそのユーザーとのみペアリングされます。

実際には、両方のクエリは同じ実行計画を取得します:

+----------------------------------------------------------------+
| QUERY PLAN                                                     |
+----------------------------------------------------------------+
| Hash Join  (cost=37.00..60.52 rows=1070 width=88)              |
| Hash Cond: (p.user_id = u.id)                                  |
| ->  Seq Scan on posts p  (cost=0.00..20.70 rows=1070 width=48) |
| ->  Hash  (cost=22.00..22.00 rows=1200 width=40)               |
| ->  Seq Scan on users u  (cost=0.00..22.00 rows=1200 width=40) |
+----------------------------------------------------------------+

表現力の問題点

これは実際には非常に有用であり、多くの ORM(オブジェクト・リレーションシップ・マッパー)やクエリビルダが抱える「表現力の問題」を解決する方法を提供します。すなわち、クエリの組み立て(合成)が困難であるという問題です。

他のクエリビルダーにおける多くの組み込み技術は、要するに次のいずれかの方法に行われていると私は考えます。

  1. クエリビルダーオブジェクトを関数列を通じて渡し、各関数が結合対象のテーブルと
    WHERE
    節を追加していく手法。
  2. サブクエリを返す関数を用意し、その結果に対して自分で結合処理を行う手法。

どちらも非常に優れたアプローチとは言えません:

  • 前者は恐らく動的型付け言語でのみ機能します。
  • 後者では、
    posts_of_users(user_id)
    [Post]
    といった「関数」を提供する方法がありません。

さらに悪いのは、ORM が関係性を非表示に抽象化してしまうことです。最初は

select(User).with(Post)
と書くと自動で JOIN が構築されたり、多対多(M2M)の結合を処理できたりするのは魅力的ですが、すぐに何かを更新する必要があるとすると、自分で結合テーブルを管理するよりもさらに退屈になります。私は以前、通常のインターフェースではまずすべての値を読み込んでからエントリーを追加または削除する必要があり、そうでないと ORM があなたのデータを挿入前にすべてを削除してしまうように指示してしまったり、M2M の一方側で重複を作成しようとして失敗したりする光景を目撃したこともあります。

また、どの

.with()
メソッドを持つ ORM を使用する場合でも、型付けされた言語においては地獄の果てです:

  • 全ての可能な組み合わせに対して
    FooWithBar
    といった型を呼び出すようなマクロマジックを使用するか。
  • それとも、
    With<Foo, Bar>
    といった型を生み出し、それが
    With<With<Foo, Bar>, Baz>
    といった恐ろしくネストされた型化されてしまい、IDE の助けなしには扱えない状態に追い込まれるか。

また、純粋な SQL クエリ(別ファイルまたはインライン)を書いてコンパイル時のコード実行でホスト言語用の関数を生成する「バインド生成(generate bindings)」アプローチ(例:sqlx)もあまり好きではありません。これでは結局 SQL を書き直さなければなりませんが、少なくとも型安全性は保てます。しかしながら、これは他のクエリ構築形式と比較して厳密に組み込み性(composability)が劣ります。

理想的な eDSL

私は以下の全てを満たす種類のクエリビルダーライブラリの提示を望んでいます:

  • 表現力豊かであること:複雑な結合を行うクエリは不明瞭であってはいけません。理想としては、eDSL で表した場合の方が SQL の方がより明確に表現されるべきです。
  • 組み合わせ可能であること:クエリは再利用性があり、パラメータ化可能であるべきです。
  • 型安全性であること:eDSL はホスト言語の型システム内で動作し、クエリが正しく型付けされていることを保証します。
  • 常に有効な SQL を生成すること:これは集計操作において特に有用であり、集計演算子を使用すると、使用该されるクエリの部分全体の要件が変わってしまうためです。
  • ユーザー定義の型と連携すること:ORM の魅力の一つは、データベースとの互換性を確保するためのユーザー型のボイラープレート処理を ORM が行う点にあると考えています。

既存のソリューション:Haskell の Rel8

私が最初に遭遇した、Later Join を用いて組み合わせ可能にクエリを構築する方法を提供するライブラリは、Haskell ライブラリの

Rel8
です。これは以下のような外観です。

postsForUser :: Expr UserId -> Query (Post Expr)
postsForUser userId = do
  post <- each postSchema
  where_ $ post.userId ==. userId
  pure post
  
usersAndPosts :: Query (User Expr, Post Expr)
usersAndPosts = do
  user <- each userSchema
  post <- postsForUser user.id
  pure (user, post)

このインターフェースは非常に表現力豊かで、実際にはすでにホスト言語内に存在するデータを操作しているかのような感覚を受けますが、実際には SQL クエリを構築しており、以下のような結果になります。

SELECT
  CAST("id0_1" AS "int4") as "_1/id",
  CAST("name1_1" AS "bpchar"(1)[]) as "_1/name",
  CAST("id0_3" AS "int4") as "_2/id",
  CAST("user_id1_3" AS "int4") as "_2/userId",
  CAST("body2_3" AS "bpchar"(1)[]) as "_2/body"
FROM (
  SELECT *
  FROM (
      SELECT "id" as "id0_1", "name" as "name1_1"
      FROM "user" AS "T1"
    ) AS "T1",
    LATERAL
      (
        SELECT "id" as "id0_3", "user_id" as "user_id1_3", "body" as "body2_3"
        FROM "post" AS "T1"
      ) AS "T2"
  WHERE (("user_id1_3") = ("id0_1"))
) AS "T1"

ポイントとして、

Expr UserId
は実際には
UserId
を含んでおらず、代わりに(この例の場合)SQL 表現
id0_1
を含んでいます。これはその後、
postsForUser
で他の SQL 表現(リテラルなど)のように使用できます。

一方、

Query
は単なる
SELECT ...
節であり、
do
ブロックの各行がクエリを追加し、追加されるのは
CROSS JOIN LATERAL ...
です。また、
where_
は単に構築中のクエリに
WHERE
節を導入します。

Haskell のスコープ問題

さて、これはもちろん明らかな問題を持っています:

id0_1
は有効なのは、そのカラムを導入するサブクエリの兄弟サブクエリ内であり、かつ構文上はその後ろにある位置にあるときのみです。通常、クエリビルダーでは無効なクエリの生成をできるだけ困難にするのが望ましいとされ、そのようなデバッグは常に大変なものだからです。

Haskell ではこれが本質的に解決されています:

Expr
の値は
Query
モナドの「内部」にのみ存在し、その外に取り出すことはできません。そのため、
Expr
は導入された後、かつ導入されたスコープ(同じ
Query
内)またはそれよりも深いスコープ(関数を呼び出して作成された別の
Query
内)でのみ使用できるという制約があります。

私の Rust 実装:
rust-rel8

さて、ここからは Haskell についての話は停止し、私が本当に話したいのは Rust で書かれたライブラリ

rust-rel8
(魅力的なプロジェクト名が思いつくまでその名前で呼ぶでしょう)です。これは Rel8 の動作を再現するものです。

私のライブラリは Rel8 と非常に似ていますが、Rust 版です。

fn posts_of_user(user_id: Expr<i32>) -> Query<Post> {
    query::<Post<ExprMode>>(|q| {
        let post = q.q(Query::each(&Post::SCHEMA));
        q.where_(user_id.equals(post.user_id.clone()));
        post
    })
}

let q = query::<(User<ExprMode>, Post<ExprMode>)>(|q| {
    let user = q.q(Query::each(&User::SCHEMA));
    let post = q.q(posts_of_user(user.id.clone()));

    (user, post)
})
.order_by(|x| (x.clone(), sea_query::Order::Asc));

let rows = q.all(&mut *pool).await.unwrap();

さらにクールなことに、

posts_of_user
の結果を集計することができ、データベースから出力される結果が
(User, Vec<Post>)
になるようにすることができます。

let q = query::<(User<ExprMode>, ListTable<Post<ExprMode>>)>(|q| {
    let user = q.q(Query::each(&User::SCHEMA));
    let posts = q.q(posts_of_user(user.id.clone()).many());
    // その .many() が Query<T> を Query<ListTable<T>> に変換します

    (user, posts)
})
.order_by(|x| (x.0.name.clone(), sea_query::Order::Asc));

let rows: Vec<(User, Vec<Post>)> = q.all(&mut *pool).await.unwrap();

さらに驚くべきことに、左外部結合は

.optional()
を使用して作成できます。

fn latest_post_of_user(user_id: Expr<i32>) -> Query<MaybeTable<Post>> {
    query::<Post<ExprMode>>(|q| {
        let post = q.q(Query::each(&Post::SCHEMA));
        q.where_(user_id.equals(post.user_id.clone()));
        post
    })
    .order_by(|x| (x.id.clone(), sea_query::Order::Desc))
    .limit(1)
    .optional()
}

let q = query::<(Expr<String>, Expr<Option<String>>)>(|q| {
    let user = q.q(Query::values(demo_users.shorten_lifetime()));
    let post = q.q(latest_post_of_user(user.id.clone()));
    // ここでの `post` は `MaybeTable<Post>` です。これより `Expr<Option<T>>` を投影するか、
    // またはクエリからそのまま返すこともでき、後者の場合はデコード後に `Option<T>` が得られます。
    let post_content = post.project(|p| p.contents.clone());
    
    (user.name, post_content)
})
.order_by(|x| (x.clone(), sea_query::Order::Asc));

let rows = q.all(&mut *pool).await.unwrap();

assert_eq!(
    vec![
        ("Huldra".to_owned(), None),
        ("Leschy".to_owned(), Some("Quak!".to_owned())),
        ("Undine".to_owned(), Some("Croak".to_owned()))
    ],
    rows
)

さらに、独自のテーブルを宣言することもできます。

#[derive(Debug, PartialEq, rust_rel8_derive::TableStruct)]
struct User<'scope, Mode: TableMode = ExprMode> {
    id: Mode::T<'scope, i32>,
    name: Mode::T<'scope, String>,
}

impl<'scope> User<'scope, NameMode> {
    const SCHEMA: TableSchema<Self> = TableSchema {
        name: "users",
        columns: User {
            id: "id",
            name: "name",
        },
    };
}

#[derive(Debug, PartialEq, rust_rel8_derive::TableStruct)]
struct Post<'scope, Mode: TableMode = ExprMode> {
    id: Mode::T<'scope, i32>,
    user_id: Mode::T<'scope, i32>,
    body: Mode::T<'scope, String>,
}

impl<'scope> Post<'scope, NameMode> {
    const SCHEMA: TableSchema<Self> = TableSchema {
        name: "posts",
        columns: Post {
            id: "id",
            user_id: "user_id",
            body: "body",
        },
    };
}

.aggregate
ビルダーも、SQL でそれらを作成する際に通常遭遇する苦労なしに集計を構築できるように設計されています。

let q = query::<Two<_, i32, i32>>(|q| {
    let a = q.q(Query::values([
        Two { a: 1, b: 1 },
        Two { a: 1, b: 2 },
        Two { a: 1, b: 3 },
        Two { a: 1, b: 4 },
        Two { a: 2, b: 1 },
        Two { a: 2, b: 2 },
        Two { a: 3, b: 1 },
    ]));
    a
})
.aggregate::<(Expr<i32>, ListTable<Expr<i32>>, ListTable<Two<_, i32, i32>>)>(|a, e| {
    let x = a.group_by(e.a.clone());
    let y = a.array_agg(e.a.clone().add(Expr::lit(1i32)));
    let as_array = a.array_agg(e);
    
    // .aggregate は、出力に含まれるすべてのものが `a` を通過したものでなければならず、
    // したがってグループ化の一部として使用されるか、集計関数の一部である必要があることを強制します。
    (x, y, as_array)
});

let rows = q.all(&mut *pool).await.unwrap();

assert_eq!(
    vec![
        (
            1,
            vec![2, 2, 2, 2],
            vec![
                Two { a: 1, b: 4 },
                Two { a: 1, b: 3 },
                Two { a: 1, b: 2 },
                Two { a: 1, b: 1 }
            ]
        ),
        (3, vec![4], vec![Two { a: 3, b: 1 }]),
        (2, vec![3, 3], vec![Two { a: 2, b: 2 }, Two { a: 2, b: 1 }])
    ],
    rows
);

実装の詳細

ここでの実行可能なマクロは、ライブラリがあなたのテーブルの各カラムを巡回する方法を知るために必要ないくつかのトレイトを実装しています。最も魔法的な部分は、

TableMode
に依存してフィールドの型を
U
,
Expr<U>
, および
String
に切り替えるこの
Mode::T<'scope, U>
ADT にあります。これにより、eDSL 内でユーザー定義の型を使用できると同時に、結果値の外でも使用できるようになります。

当然のことながら、Rust のため Haskell 版より少し冗長ですが、Rel8 の実際の API は Rust への変換は比較的小さいです。

それはどのように機能しているか?

基本概念:
Expr
Query

まず、スカラー表現を表す型が必要です。これには

Expr
という名前を付けましょう。

pub struct Expr<'scope, T> {
    expr: ExprInner,
    _phantom: PhantomData<(&'scope (), T)>,
}

ここで

'scope
は表現のスコープルールを強制するために使用されるライフタイムです(現在は無視して構いませんし、後で説明します)。
PhantomData
は必要であり、これは
Expr
が実際には
T
'scope
も含まないためです。

ExprInner
は単純に私が下流で使用しているクエリビルダーライブラリ(
sea_query
)の AST をラップするものであり、内部に含まれるカラムへの参照を巡回することを可能にします。

この

Expr
には、スカラー上で一般的な SQL 操作に対応するいくつかの方法があります。

impl<'scope, T> Expr<'scope, T> {
    /// 任意のエンコード可能な値からリテラル値を作成します。
    pub fn lit(value: T) -> Self
    where
        T: Into<sea_query::Value>,
    {
        Self::new(ExprInner::Raw(sea_query::Expr::value(value.into())))
    }

    fn binop<U>(
        self,
        other: Self,
        binop: Arc<dyn Fn(sea_query::SimpleExpr, sea_query::SimpleExpr) -> sea_query::SimpleExpr>,
    ) -> Expr<'scope, U> {
        Expr::new(ExprInner::BinOp(
            binop,
            Box::new(self.expr),
            Box::new(other.expr),
        ))
    }

    /// SQL 等価演算子
    pub fn equals(self, other: Self) -> Expr<'scope, bool> {
        self.binop(
            other,
            Arc::new(|a, b| a.binary(sea_query::BinOper::Equal, b)),
        )
    }
    
    /// SQL 数値加算
    pub fn add(self, other: Self) -> Self {
        self.binop(other, Arc::new(|a, b| a.binary(sea_query::BinOper::Add, b)))
    }

    /// `nextval('name')` を生成して適切に動作させます。
    pub fn nextval(name: &str) -> Self {
        Self::new(ExprInner::Raw(
            sea_query::Func::cust("nextval").arg(name.to_owned()).into(),
        ))
    }
}

クエリを表すには

Query
を使用します。

/// 行型データとして型 `T` の結果を生成する SQL SELECT 文を表す値。
#[derive(Clone)]
pub struct Query<T> {
    // テーブル名とカラムを一意にするために使用するユニークな ID
    binder: Binder,
    expr: sea_query::SelectStatement,
    inner: T,
    siblings_need_random: bool,
}

これはそれ自体は特に興味深いものではなく、実際には SELECT 文の AST と、ライブラリがクエリによって生成されるカラムを知ることができる式のみを含んでいます。魔法が起こるのは

query
です。

pub struct Q<'scope> {
    queries: Vec<(TableName, ErasedQuery)>,
    filters: Vec<ExprInner>,
    binder: Binder,
    _phantom: PhantomData<&'scope ()>,
}

impl<'scope> Q<'scope> {
    pub fn q<T: ForLifetimeTable + Table>(&mut self, query: Query<T>) -> T::WithLt<'scope> {
        let binder = Binder::new();
        let name = TableName::new(binder);
        let (erased, mut inner) = query.erased();
        self.queries.push((name.clone(), erased));
        insert_table_name(&mut inner, name);
        inner.with_lt(&mut WithLtMarker {})
    }

    pub fn where_<'a>(&mut self, expr: Expr<'a, bool>)
    where
        'scope: 'a,
    {
        self.filters.push(expr.expr);
    }
}

pub fn query<'outer, T: ForLifetimeTable + Table + 'outer>(
    f: impl for<'scope> FnOnce(&mut Q<'scope>) -> T::WithLt<'scope>,
) -> Query<T> {
    let mut q = Q {
        binder: Binder::new(),
        filters: Vec::new(),
        queries: Vec::new(),
        _phantom: PhantomData,
    };

    let mut e = f(&mut q);

    let mut iter = q.queries.into_iter();
    let mut table = sea_query::Query::select();

    // 最初のクエリは SELECT の「主要」クエリとなる
    if let Some((first_table_name, first)) = iter.next() {
        table.from_subquery(first.expr, first_table_name);
    };

    // リストにある残りのすべてのテーブルに対するラテラル結合
    for (table_name, q) in iter {
        table.join_lateral(
            // 通常は CROSS JOIN ですが、sea_query は CROSS JOIN の `ON` を省略することをサポートしていないため(:
            // そして CROSS JOIN は INNER JOIN ON TRUE に相当します
            sea_query::JoinType::InnerJoin,
            expr,
            table_name,
            sea_query::Condition::all(),
        );
    }

    for filter in q.filters {
        table.and_where(filter.render());
    }

    // クエリは現在カラムを選出しておらず、この関数は出力テーブル内の式を巡回し、
    // 各式を `<expr> AS <name>` の形式で SELECT に追加し、
    // その中で式を `<name>` カラム参照に置き換えます。
    subst_table(&mut e, TableName::new(q.binder), &mut table);
    
    let e = ForLifetimeTable::unwith_lt(e, &mut WithLtMarker {});
    Query::new(q.binder, table.to_owned(), e)
}

ライフタイムの役割

ここには特に何も起こっていません;実際に行うべきことは、衝突を引き起こさないようにカラムを適切に再命名することだけです。それ以外には、最初の言及されたクエリから始めて、各サブクエリに対してラテラル結合を導入し、その後ビルドしているクエリのプロジェクトされたカラムとして結果テーブルのカラムを巡回して挿入するだけという単純なものです。

おそらく

query
の型シグネチャ(および
'scope
ライフタイムパラメータを持つすべてのもの)をよく見ていただろうと思います。これは多くをしており、その理由は、借用チェッカーを使用してクエリのスコープルールを強制できるようになったためです。

query
の呼び出し関数の型には
for <'scope>
があり、これは渡された関数がそのライフタイムに対してジェネリックであり、ライフタイムについて他に何も知ることはできず、与えられた
Q<'scope>
を受け取り、必ず
T
とそのライフタイムを持つ何かを返さなければならないことを意味します。ただし、
query
がクエリを処理した後、ライフタイムは
'outer
に切り替えられ、これは
query
のライフタイムパラメータであるため、ユーザーがこれをどのように選択するかを選択できます。これは理にかなっています:
Query
はデータベースで実行できる独立した完全な SELECT クエリであり、したがってライフタイムの追跡は必要ありません。理想としては
'static
になるべきですが、このライフタイムをパラメータ化した
WithLt<'lt>
GAT を使用することの不運な副作用として、それは
'lt
で不変であるため、実際には
&'static YourTable
と道徳的によく似ているにもかかわらず、
YourTable<'static>
のライフタイムを短くすることはできません。そのため、ユーザーが多量の手動のライフタイム短縮を行う必要がないようにするために、
query
'outer
ライフタイムにジェネリックにしたことを選んだのです。

もう一つわずかに不運な事実として、

query
のコールバック内で
T
T::WithLt<...>
として登場することで、Rust は
T
を使用から推論できないという問題があります。ユーザーは常にどこかでそれを明記する必要があります(幸いにもユーザーはライフタイムの名称を指定する必要はありません)。完璧な世界であれば、私たちは以下の様に書くことができたでしょう:

fn query<'outer, T: for<'a> (T<'a>: Table), F: FnOnce(...) -> T<'scope>>(...) -> T<'outer> 

つまり、Rust がクロージャーから返される

T
query
から返される
T
が同じであることがわかるように。しかしながら、これは完璧な世界ではありません。

テーブル操作のためのトレイト

さて、この関数はすべての可能なテーブルに対してジェネリックである必要があります(「テーブル」とは、単一要素の

Expr<T>
(T, ...)
のタプルで
T: Table
またはユーザー定義のテーブルを指します)。また、API が過度に制限的であることは望んでいません(テーブルを
Table<(i32, UserType)>
のようなものとして持つことができますが、これは標準的なフィールドアクセスを使用してサブテーブルとカラムを投影することを禁止することになります)。つまり、ライブラリが実行できる操作をモデル化するためにいくつかのトレイトが必要となります。これらは以下の通りです:

  • テーブル内の列を訪問する:
    trait Table
  • テーブル内のライフタイムを変更する:
    trait ForLifetimeTable
  • テーブルのライフタイムを短縮できるようにする方法:
    trait ShortenLifetime
  • ユーザー定義テーブルのモードを変更する:
    trait TableHKT
  • ユーザー定義テーブルのフィールドに自然変換を適用する方法:
    trait MapTable
Table

Table
は非常にシンプルです;それは単にテーブル内のすべての列に対していくつかの実行するコールバックを実行する方法を表しています。
Expr<T>
については、単に内部の
ErasedExpr
上でコールバックを呼び出すだけであり、タプルの場合は順次各サブテーブルで訪問を行うだけです。

pub trait Table {
    /// データベースから読み込まれたときのこのテーブルの行が持つ値。
    type Result;

    /// テーブル内の各式を訪問する。
    ///
    /// [Table::visit], [Table::visit_mut]、および [MapTable] のすべてのメソッドの間で常に同じ順序と数の式が訪問される必要があります。
    fn visit(&self, f: &mut impl FnMut(&ErasedExpr), mode: VisitTableMode);

    /// テーブル内の各式を可変参照を持って訪問する。
    ///
    /// [Table::visit], [Table::visit_mut]、および [MapTable] のすべてのメソッドの間で常に同じ順序と数の式が訪問される必要があります。
    fn visit_mut(&mut self, f: &mut impl FnMut(&mut ErasedExpr), mode: VisitTableMode);
}
ForLifetimeTable

ForLifetimeTable
は、テーブル内のネストされた
Expr<'scope, T>
型のライフタイムをライブラリに置換することを可能にします:

pub trait ForLifetimeTable {
    /// このテーブルのライフタイムを `'lt` に置換する。
    type WithLt<'lt>: ForLifetimeTable + Table + Sized;

    fn with_lt<'lt>(self, marker: &mut WithLtMarker) -> Self::WithLt<'lt>;
}

impl<'scope, T: Value> ForLifetimeTable for Expr<'scope, T> {
    type WithLt<'lt> = Expr<'lt, T>;

    fn with_lt<'lt>(self, _marker: &mut WithLtMarker) -> Self::WithLt<'lt> {
        Expr::new(self.expr)
    }
}
ShortenLifetime

ShortenLifetime
ForLifetimeTable
と似ていますが、その目的はライブラリのユーザーがテーブルのライフタイムを短縮する必要がある場合に使用することです。通常 Rust ではこれが自動で行われますが、テーブルではライフタイムが不変であるため、Rust はそれを自動で行いません。代わりに、ユーザーは
.shorten_lifetime()
を呼び出す必要があります。

pub trait ShortenLifetime {
    type Shortened<'small>
    where
        Self: 'small;

    fn shorten_lifetime<'small, 'large: 'small>(self) -> Self::Shortened<'small>
    where
        Self: 'large;
}

impl<'scope, T> ShortenLifetime for Expr<'scope, T> {
    type Shortened<'small>
        = Expr<'small, T>
    where
        Self: 'small;

    fn shorten_lifetime<'small, 'large: 'small>(self) -> Self::Shortened<'small>
    where
        Self: 'large,
    {
        Expr::new(self.expr)
    }
}
TableHKT

TableHKT
は、ライブラリが異なる
TableMode
間でユーザー定義のテーブルについて話することを可能にします。

pub trait TableHKT {
    /// このテーブルの現在のモード。
    type Mode: TableMode;

    /// モードを別のものに置換する。
    type InMode<Mode: TableMode>;
}

impl<'scope, T: TableMode> TableHKT for User<'scope, T> {
    type InMode<Mode: TableMode> = User<'scope, Mode>;
    type Mode = T;
}
MapTable

最後に、ユーザー定義の型のフィールドをマッピングする

MapTable
です。

pub trait MapTable<'scope>: TableHKT {
    /// テーブルの各フィールドをマッピングする
    ///
    /// [Table::visit], [Table::visit_mut]、および [MapTable] のすべてのメソッドの間で常に同じ順序と数のフィールドが訪問される必要があります。
    fn map_modes<Mapper, DestMode>(self, mapper: &mut Mapper) -> Self::InMode<DestMode>
    where
        Mapper: ModeMapper<'scope, Self::Mode, DestMode>,
        DestMode: TableMode;

    /// テーブルの各フィールドを参照を持ってマッピングする
    ///
    /// [Table::visit], [Table::visit_mut]、および [MapTable] のすべてのメソッドの間で常に同じ順序と数のフィールドが訪問される必要があります。
    fn map_modes_ref<Mapper, DestMode>(&self, mapper: &mut Mapper) -> Self::InMode<DestMode>
    where
        Mapper: ModeMapperRef<'scope, Self::Mode, DestMode>,
        DestMode: TableMode;

    /// テーブルの各フィールドを可変参照を持ってマッピングする
    ///
    /// [Table::visit], [Table::visit_mut]、および [MapTable] のすべてのメソッドの間で常に同じ順序と数のフィールドが訪問される必要があります。
    fn map_modes_mut<Mapper, DestMode>(&mut self, mapper: &mut Mapper) -> Self::InMode<DestMode>
    where
        Mapper: ModeMapperMut<'scope, Self::Mode, DestMode>,
        DestMode: TableMode;
}
TableMode
とユーザー定義の型

MapTable
を理解するには、まず
TableMode
とユーザー定義の型を見ておく必要があります。

TableMode
は、いくつかのタイプをモードに応じて切り替えることを目的とする GAT(Generalized Algebraic Type)トレイトです。

pub trait TableMode {
    /// GAT は、結果タイプが `V` を含んでいるかどうかは不定です。
    type T<'scope, V>;
}

impl TableMode for NameMode {
    /// カラム名を表す文字列。
    type T<'scope, V> = &'static str;
}

impl TableMode for ValueMode {
    type T<'scope, V> = V;
}

impl TableMode for ValueManyMode {
    type T<'scope, V> = Vec<V>;
}

impl TableMode for ExprMode {
    type T<'scope, V> = Expr<'scope, V>;
}

impl TableMode for EmptyMode {
    type T<'scope, V> = ();
}

ユーザー定義の型は以下のように定義されます:

struct User<'scope, Mode: TableMode = ExprMode> {
    id: Mode::T<'scope, i32>,
    name: Mode::T<'scope, String>,
}

つまり、

User<NameMode>
{ id: &str, name: &str }
であり、
ValueMode
では
{ id: i32, name: String }
になります。クエリ内で使用される場合、テーブルは
ExprMode
にあり、
{ id: Expr<i32>, name: Expr<String> }
となります。これにより、
q.where_(user.id.equals(user_id))
と書けるようになります。

モードマッパー

もちろん、このパターンには解決すべき問題があります:いくつかの

User<NameMode>
が与えられたとき、ライブラリがすべてのフィールドを訪問してカラム名を引き出し、そして何らかの方法でクエリ内で使用できる
User<ExprMode>
を生成する方法是どうすればよいのでしょうか。理想的には、ユーザーが
fn name_to_expr_mode(self) -> (Vec<String>, User<ExprMode>)
といった無限のトレイトとメソッドを実装することを要求せずに、これを行うことができれば理想的です。

答えはすべてこれらを実現することが

MapTable
トレイトによってモデル化でき、単に型変化する関数をすべてのフィールドに対して呼び出す方法を提供するだけであり、すべての呼び出しを通じてステータスが渡されるという点にあることにあります。実際、ライブラリは
MapTable
を使用してユーザー定義のテーブルのための
Table
を実装しています。

私が検討する

MapTable
のメソッドは
map_modes_ref
であり、不変参照を介して各フィールドを訪問し、宛先モードを持つ新しいテーブルを生成します。

fn map_modes_ref<Mapper, DestMode>(&self, mapper: &mut Mapper) -> Self::InMode<DestMode>
where
    Mapper: ModeMapperRef<'scope, Self::Mode, DestMode>,
    DestMode: TableMode;

すべての作業は

ModeMapperMut
によって行われます:

pub trait ModeMapperRef<'scope, SrcMode: TableMode, DestMode: TableMode> {
    /// `SrcMode` から `DestMode` にマッピングし、値を参照として受け取る
    fn map_mode_ref<V>(&mut self, src: &SrcMode::T<'scope, V>) -> DestMode::T<'scope, V>
    where
        V: Value;
}

ユーザーが

MapTable
を実装するには、単に次のような実装を書くだけです。

impl<'scope, Mode: TableMode> MapTable<'scope> for MyTable<'scope, Mode> {
    fn map_modes_ref<Mapper, DestMode>(&self, mapper: &mut Mapper) -> Self::InMode<DestMode>
    where
        Mapper: ModeMapperRef<'scope, Self::Mode, DestMode>,
        DestMode: TableMode,
    {
        let id = mapper.map_mode_ref(&self.id);
        let name = mapper.map_mode_ref(&self.name);
        let age = mapper.map_mode_ref(&self.age);
        User { id, name, age }
    }
}

この技術によって、ライブラリは

MapTable
を使用してユーザー定義の型のフィールドタイプ変換巡回を行うことができます。実際には、ユーザーが実装したコールバックを選択呼び出しながら、ユーザーによって知られているフィールドタイプでインスタンス化されたプロキシを設定しています。

ライブラリの側では、私たちは以下のように見える「マッパー」を書くことができます。

/// [NameMode] でテーブルのカラム名を読み取り SELECT に追加し、その後列を参照する式を生成するマッパー。
struct NameToExprMapper {
    binder: Binder,
    query: sea_query::SelectStatement,
}

impl<'scope> ModeMapperRef<'scope, NameMode, ExprMode> for NameToExprMapper {
    fn map_mode_ref<V>(
        &mut self,
        src: &<NameMode as TableMode>::T<'scope, V>,
    ) -> <ExprMode as TableMode>::T<'scope, V> {
        let col_name = ColumnName::new(self.binder, src.to_string());
        self.query
            .expr_as(sea_query::Expr::column(*src), col_name.clone());

        Expr::new(ExprInner::Column(TableName::new(self.binder), col_name))
    }
}

// これはその後 Query::each に使用されてフィールド名を抽出し、
// および ExprMode テーブルを生成します。

impl<'scope, T> Query<T>
where
    T: MapTable<'scope> + TableHKT<Mode = NameMode>,
    T::InMode<ExprMode>: ForLifetimeTable + Table,
{
    /// [TableSchema] が与えられたとき、各行のすべてのカラムを選出するクエリを構築します。
    pub fn each(schema: &TableSchema<T>) -> Query<T::InMode<ExprMode>> {
        let binder = Binder::new();
        let mut query = sea_query::Query::select();
        query.from(schema.name);

        let mut mapper = NameToExprMapper { binder, query };

        let expr = schema.columns.map_modes_ref(&mut mapper);

        Query::new(binder, mapper.query, expr)
    }
}

機能の概要

それによって、クエリ eDSL を構築するために必要なすべての部品が揃いました:

  • MapTable
    :
    NameMode
    テーブルのカラム名を抽出して
    ExprMode
    テーブルを構築でき、そしてデータベース上でクエリを実行した後で再度使用して、各結果行から対応する値を脱シリアル化することで
    ExprMode
    テーブルを
    ValueMode
    に変換できます。
  • TableHKT
    ForLifetimeTable
    : Rust のライフタイムによって強制される制約を維持しながら任意のテーブル上で動作する関数を提供できます。
  • Query
    ,
    query
    , および
    Q
    : ユーザーはクエリを組み合わせて非常に自由に扱えるが、コードがコンパイルされれば有効なクエリを持つことが保証される。
  • Expr<T>
    : ユーザーはスカラー表現を構築でき、それらを他のクエリを返す関数に渡すことができる。

まだ多くの作業がありますが、この強力な抽象化を提供しながら実際にそれほど多くのコードを必要としない小さなクレートを作ることができたのは非常に嬉しかったです。

同じ日のほかのニュース

一覧に戻る →

2026/04/29 23:34

ゼッド 1.0

## Japanese Translation: Zed は公式にバージョン 1.0 をリリースし、多くの開発者が Mac、Windows、Linux 上でこの高性能なコードエディタを安心して利用できる重要な転換点を迎えました。Rust で完全構築され、GPU による加速レンダリングを採用する Zed は、従来のデスクトップアプリモデル(Electron など)を手放し、ゲーム環境のような優れた速度を実現するために設計された環境へと移行しました。独自に GPUI ライブラリ、CRDT を基盤とする DeltaDB データベース、シェーダーなどスタック全体を深く制御することで、競合には真似できないパフォーマンスを発揮しており、5 年で数百万行のコードを処理し、千以上のバージョンをリリースしてきました。バージョン 1.0 では数十の言語に対応し、Git 統合、SSH リモート、デバッガー、レインボー括弧などをサポートしており、すでに数十万人のデイリーユーザーに信頼されています。AI ネイティブなエディタとしての Zed は、Claude Agent、Codex、OpenCode、Cursor などのエージェントに対して Agents Client Protocol を用いてキーストローク粒度の予測とともに複数のエージェントを並行して統合しており、DeltaDB はチームメンバーが他者とエージェントとの会話を招待し、生成コンテキスト内で直接エイジェントコードをレビュー・発展させることを可能にします。このリリースの後、Zed は每周アップデートを継続し、人間と AI エージェントが文字レベルでリアルタイムに協業する未来へと向けられていきます。さらに、エンタープライズのニーズをサポートするために中央集権的な請求、ロールベースのアクセス制御、チーム管理、セキュリティ制御を提供する新たな「Zed for Business」のオファーも間もなく登場し、効率的なソフトウェア shipping において不可欠なツールとしての地位を確固たるものにします。現在、数十万人の開発者が Zed に依存して日々ソフトウェアを shipping し続けており、その職人技とパフォーマンスへの姿勢にチームは自信を抱いています。

2026/04/30 3:13

コピー失敗 – CVE-2026-31431

## Japanese Translation: CVE-2026-31431「Copy Fail」は、Xint Code による自動スキャンで発見された重大なコンテナエスケープ脆弱性であり、Linux システム上の何らかの未特権ユーザーが root に権限を昇級することを可能にします。この欠陥は 2017 年に追加された `algif_aead` モジュールにおける論理エラーに起因しており、タイミングの問題やレース条件が存在しない直線の攻撃経路によってデータ漏洩を引き起こします。2026-04-29 に公開され、2017 年以来のほぼすべての主流の Linux ディストリビューションに影響を与えており、Ubuntu、RHEL、Amazon Linux、SUSE、Debian、Arch、Fedora などを含むスタンドアロンの 732 バイトのProof-of-Conceptスクリプトによって確認されています。 **重大性と範囲:** * **高リスク:** マルチテナントホスト、Kubernetes クラスター、CI リナラー、クラウド SaaS 環境。 * **中リスク:** シングルトナントサーバー。 * **比較的低リスク:** シングルユーザーのノートパソコン(権限昇級のみ)。 **緩和措置と影響:** 管理者は直ちにメインラインの修正(コミット `a664bf3d603d` で特定)を適用する必要があります。緊急的な臨時対策として、脆弱なモジュールを無効化するために `/etc/modprobe.d/disable-algif.conf` を使用し、`rmmod algif_aead` を実行してください。AF_ALG の無効化は、それを明示的に使用するアプリケーション(例:afalg エンジンを使用する OpenSSL)に影響を与えるものの、dm-crypt、LUKS、IPsec、SSH などのコアサービスには影響しません。非信頼のワークロードについては、パッチの有無に関わらず追加の防御層として seccomp を使用して AF_ALG ソケットの作成をブロックすることをお勧めします。 ## Text to translate The original summary is well-written, clear, and comprehensive. No improvement is strictly necessary, but a slightly more structured version below offers better readability while retaining all key points. ## Improved Summary: CVE-2026-31431 "Copy Fail" A critical container escape vulnerability, CVE-2026-31431 ("Copy Fail"), discovered by Xint Code via automated scanning, allows any unprivileged user on Linux systems to escalate privileges to root. The flaw stems from a logic error in the `algif_aead` module—an optimization added in 2017—which enables data leakage through a straight-line attack path without timing issues or race conditions. Disclosed publicly on 2026-04-29, the vulnerability affects nearly all mainstream Linux distributions since 2017, confirmed across Ubuntu, RHEL, Amazon Linux, SUSE, Debian, Arch, Fedora, and more via a standalone 732-byte proof-of-concept script. **Severity & Scope:** * **High Risk:** Multi-tenant hosts, Kubernetes clusters, CI runners, and cloud SaaS environments. * **Medium Risk:** Single-tenant servers. * **Lower Risk:** Single-user laptops (privilege escalation only). **Mitigation & Impact:** Administrators must urgently apply the mainline fix identified by commit `a664bf3d603d`. As an immediate temporary measure, disable the vulnerable module using `/etc/modprobe.d/disable-algif.conf` and `rmmod algif_aead`. While disabling AF_ALG impacts applications explicitly using it (e.g., OpenSSL with the afalg engine), core services like dm-crypt, LUKS, IPsec, and SSH remain unaffected. For untrusted workloads, blocking AF_ALG socket creation via seccomp is recommended as an additional defense layer regardless of patch status.

2026/04/30 6:58

ドイツは現在、世界最大の弾薬製造国となっています。

## Japanese Translation: ラインメタルはドイツの軍事生産能力を劇的に拡大し、同国を世界トップクラスの常规兵器製造国として確立しました。この転換は、主に供給量の劇的な増加——例えば榴弾の生産速度が以前から10倍に向上し、軍用トラックの年間生産量は600台から4,500台へと増大した——によって国内防衛の緊急な需要を満たすことを目的として推進されています。これらの拡大努力はロシアの侵攻に伴うドイツの工業基盤の変革を導き、連邦国防軍(Bundeswehr)を欧州最強の勢力と位置づけることを目指していますが、新しい生産能力は特定の分野で現在の米国の生産水準を上回ることを可能にしますが、必ずしも米国よりも大量の在庫を保有しているわけではないことを意味しません。 この増大を支援するため、ラインメタルは35万件以上の雇用申請(うちドイツからの申請が25万件)を受け、急速に労働力を拡大しています。同社は2030年までに直接雇用を7万人に増やし、既存の1万1,500社のサプライヤーネットワーク全体でさらに21万人の追加ポストを設けることを期待しています。そのサプライヤーの多くは既に自動車業界における専門知識を持っています。この移行は構造転換であり、縮小傾向にある自動車業界から吸収されていた雇用を防衛生産が担うものです。ラインメタルのCEO は、これらの削減により防衛兵器生産が最終的にドイツの自動車セクターの約1/3 の雇用を代替する可能性があるとの見通しを示しています。結局のところ、この再編成は深層的な産業統合と、常规弾薬および装備品における大幅に強化された生産能力を確保します。

Postgres の LATERAL ジョインは、かなり優れた eDSL(埋め込みドメイン特定言語)の実現を可能にします。 | そっか~ニュース