Closed6

(WIP)mssql(SQLServer)のDBから、Rustのstructを自動生成する

yunayuna

全体

Rustで、mssql(SQLServer)へのCRUD処理を行う要件があります。

prisma周りのモジュール群が、Rustで開発されていたり、
Rustのprisma clientなどもあり、mssqlに限らずRustでデータベース処理を行うにあたって
色々と親和性がありそうなので、この辺りを掘っていきます。

関連リンク

▼Prisma
https://www.prisma.io/docs
▼Prisma Client Rust
https://prisma.brendonovich.dev/
https://github.com/Brendonovich/prisma-client-rust
▼SeaORMのsql server版(202409時点では非公開)
https://www.sea-ql.org/SeaORM-X/

yunayuna

ざっくりの流れ

PrismaはNodeのツール(?)というイメージがあり、これまで使ったことが無かったので、
説明を読みながら進めていきます。

スタート時点での手順イメージ

  1. Prismaで、mssqlのDBに接続し、スキーマを自動的にインポートしてPrisma Schemaファイル (schema.prisma) を生成する
  2. prisma-client-rust を用いて、Prisma Schemaファイルから、Rustのコードを生成する
  3. あとはサンプル見ながらCRUDする

なんとなく1,2まではこんな感じのイメージ。

3以降は、mssql接続だとtiberiusという、これまたPrisma管理化のmssql専用ドライバcrateがあったり、
ORMやクエリビルダとしての各種crate(SeaquelとかSeaORMとか)を上手く使って処理できるのかな?ぐらいな感じ。
Dieselは一気通貫のため他ツールとの連携に向いて無さそう(かつmssqlは非対応)、
sqlxは、明示的にv0.6以降mssql対応していないと記載があるので、連携はできるけど難しいのかな?

このような認識で進めていきます。

yunayuna

まず、chatGPTに聞いてみる

prismaを使って、mssqlのデータベース構成を元に、構成ファイルを生成することはできる?

node処理系を使って進めていくようなのだが、
普段はbunを使っているので、prismaに対応しているか調査したところ、問題なさそう。
という訳で、bunを使ったコマンドに置き換えて、処理を進めていく。
https://bun.sh/guides/ecosystem/prisma

1. Prisma CLIのインストール

mkdir prisma-app
cd prisma-app
bun init

bun add -d prisma
bun add @prisma/client

bunx prisma init --datasource-provider sqlserver

これで、prismaの構成ファイルが生成される。

prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlserver"
  url      = env("DATABASE_URL")
}

2. Prismaのデータベース接続設定

prisma/schema.prisma ファイルは上記の通り自動的に生成されているので、
特に変更は不要です。
デフォルトでは、DATABASE_URLを環境変数から取得するようになっているので、
以下の形式のURLを事前にDATABASE_URLにセットしておきます。

フォーマットは、以下のような形です。
sqlserver://username:password@host:port/database
sqlserver://HOST[:PORT];database=DATABASE;user={MyServer/MyUser};password={ThisIsA:SecurePassword;};encrypt=true

今回、私の環境では、ローカル環境に接続するため、以下のようにしています。
例:
sqlserver://localhost:1433;database=SAMPLEDB;user=sa;password=Zaq12wsx;encrypt=true;TrustServerCertificate=true;

3. データベースのスキーマをインポート

次に、Prismaのスキーマを生成するために、以下のコマンドを実行します。

bunx prisma db pull

接続情報が正しければ、処理が走り、
このコマンドは、指定されたデータベースからすべてのテーブルとその構造をスキャンし、schema.prisma ファイルにスキーマを自動的に追加します。

4. スキーマの確認と修正

schema.prisma に生成されたスキーマを確認し、必要に応じて修正します。Prismaのスキーマファイルでは、リレーションシップやデータ型などを細かく設定できます。

5. Prisma Clientの生成

スキーマを定義したら、次のコマンドを実行してPrisma Clientを生成します。

bunx prisma generate

問題発覚

今回扱うDBは、テーブル名、フィールド名に日本語が含まれており、
スキーマ情報は生成されたのですが、
テーブル名に含まれる日本語を自動的に削除、
フィールド名に日本語が含まれる場合、コメントアウトされてしまいました。

model TEST________ {
  /// This field was commented out because of an invalid name. Please provide a valid one that matches [a-zA-Z][a-zA-Z0-9_]*
  // フィールド1 String @map("フィールド1") @db.VarChar(2)
  /// This field was commented out because of an invalid name. Please provide a valid one that matches [a-zA-Z][a-zA-Z0-9_]*
  // ふぃーるど2 String @map("ふぃーるど2") @db.VarChar(9)

  @@map("TESTテーブル")
}

Rustの場合、このようなマルチバイトのテーブル、フィールド名はコンパイルエラーにはならないのですが、

struct あいうえお {
    てすと: String,
    abc: i32,
}

Prisma用のschemaデータが正しく生成されないと、prisma-client-rustも使えません。
というわけで、Prismaを使う以上は、条件にあった名称に変更しないといけない様です。

なので、生成されたファイルを、強制的にスクリプトで編集するという強引なやり方を試します。
(@map で、もともとの名称とのマッピングはされているようなので、そこは安心)

yunayuna

schemaファイルの書き換え

ファイルを見ると、
@@map(xxx)で、テーブル名のマッピングが、
@map(xxx)で、フィールド名のマッピングがされているようです。

なので、

  1. @@map(xxx)を探して、元のテーブル名の頭にTを付けた名前に変更したものに、model名を更新する
  2. コメントアウトされているフィールドのコメントアウトを外す
    これをスクリプト書いて処理します。
main.rs
use std::io::{self, Write};
use std::fs::{self, OpenOptions};

fn main() {
    update_japanese_tables().unwrap();
}

fn update_japanese_tables() -> io::Result<()> {
    // 読み込むファイル名
    let input_file_path = "./schema.prisma";
    // 書き込むファイル名
    let output_file_path = "./schema_updated.prisma";

    // ファイルを読み込む
    let content = fs::read_to_string(input_file_path)?;

    // 内容を行ごとに処理
    let mut updated_content = String::new();
    let mut line_buffer = String::new();
    let mut this_model_head = String::new();

    let model_start = regex::Regex::new(r#"model.+\{"#).unwrap();
    let table_map_regex = regex::Regex::new(r#"@@map\("(.+)"\)"#).unwrap();
    
    let mut is_remove_comment_out = false;

    for line in content.lines() {
        // モデルの開始行を探す
        let line_replaced = if is_remove_comment_out{
            is_remove_comment_out = false;
            line.replacen("//","",1)
        } else {
            line.to_string()
        };

        if model_start.find(&line_replaced).is_some() {
            //前のmodel情報を更新する
            if line_buffer.len() > 0 {
                if this_model_head.len() > 0 {
                    updated_content.push_str(&this_model_head);
                    updated_content.push('\n');
                }
                updated_content.push_str(&line_buffer);
                line_buffer = String::new();
                this_model_head = String::new();
            }
            this_model_head = line_replaced.clone();

        } else if let Some(caps) = table_map_regex.captures(&line_replaced) {
            // @@mapの内容を抽出
            let table_name = caps.get(1).unwrap().as_str();
            // モデル名を更新
            this_model_head = format!("model {} {{\n", table_name);
            
            line_buffer.push_str(&line_replaced);
            line_buffer.push('\n');
        } else {
            line_buffer.push_str(&line_replaced);
            line_buffer.push('\n');
        }

        //次フィールドがコメントアウトされている場合、indexをマークしておく
        if line_replaced.contains("/// This field was commented out because of an invalid name") {
            is_remove_comment_out = true;
        }
    }

    if line_buffer.len() > 0 {
        if this_model_head.len() > 0 {
            updated_content.push_str(&this_model_head);
            updated_content.push('\n');
        }
        updated_content.push_str(&line_buffer);
    }

    // 更新された内容を新しいファイルに書き込む
    let mut output_file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(output_file_path)?;

    output_file.write_all(updated_content.as_bytes())?;
    
    println!("ファイルの更新が完了しました: {}", output_file_path);
    
    Ok(())

}

これで、上記のスキーマファイルは、以下の様に更新されました。

model TESTテーブル {
  /// This field was commented out because of an invalid name. Please provide a valid one that matches [a-zA-Z][a-zA-Z0-9_]*
   フィールド1 String @map("フィールド1") @db.VarChar(2)
  /// This field was commented out because of an invalid name. Please provide a valid one that matches [a-zA-Z][a-zA-Z0-9_]*
   ふぃーるど2 String @map("ふぃーるど2") @db.VarChar(9)

  @@map("TESTテーブル")
}
yunayuna

Prisma Client Rustを使う

Prismaのschemaデータができたので、
Prisma Client Rustを使っていきます。

https://prisma.brendonovich.dev/getting-started/installation

現在はバイナリが無いので、
Rustプロジェクトを作って、そこにcrateとして読み込んで、処理を行います。

ディレクトリ構成(作成したschema.prismaを、配置しておく

update_japanese_tables
 - migration(dir)
 - src(dir)
   - main.rs
 - schema.prisma
 Cargo.toml
Cargo.toml
[package]
name = "update_japanese_tables"
version = "0.1.0"
edition = "2021"

[dependencies]
regex = "1.10.6"
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11", default-features = false, features=["mssql"] }
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11", default-features = false, features=["mssql"] }
serde = "1.0.210"
main.rs
fn main() {
    prisma_client_rust_cli::run();
}

コマンドは、以下の通り

cargo run -- generate

エラー出まくり・・・

error: Error validating: This line is invalid. It does not start with any known Prisma schema keyword.
  -->  schema.prisma:16189
   | 

ここで気づいたのだが、これはあくまでNodeのコードジェネレータで、
Rustのコードを吐き出すわけではなさそうだ。

ここまでやったが、別の方法で仕切り直し・・・

yunayuna

他の方法

試してないが、0.6系のsqlxは、mssql featureがあるので、これを使ってやる方法もあるか?

cargo install sqlx-cli --version 0.6.3 --no-default-features --features sqlserver

https://github.com/launchbadge/sqlx/tree/main/sqlx-cli

sqlxからmssqlがサポート対象外になっている理由

https://github.com/launchbadge/sqlx?tab=readme-ov-file#sqlx

よく見たら、sqlxは対応してるが、sqlx-cliはmssqlに対応してなかった・・
これもだめ。

このスクラップは2ヶ月前にクローズされました