From 3eb22188a48ec577fa56a99befd3bd37ac4ce23a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 2 Jul 2024 00:00:02 +0900 Subject: [PATCH 001/160] Fixed ja index.md --- docs/src/main/mdoc/ja/index.md | 91 ++++++++++------------------------ 1 file changed, 25 insertions(+), 66 deletions(-) diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index 3f317d963..7a3273441 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -10,9 +10,13 @@ * [Connector](./09-Connector.md) @@@ -# LDBC +# ldbc (Lepus Database Connectivity) -**LDBC**は1.0以前のソフトウェアであり、現在も活発に開発中であることに注意してください。新しいバージョンは以前のバージョンとバイナリ互換性がなくなってしまう可能性があります。 +**ldbc**は1.0以前のソフトウェアであり、現在も活発に開発中であることに注意してください。新しいバージョンは以前のバージョンとバイナリ互換性がなくなってしまう可能性があります。 + +ldbcは、[Cats Effect 3](https://typelevel.org/cats-effect/)と[Scala 3](https://github.com/scala/scala3)による純粋関数型JDBCレイヤーを構築するためのライブラリです。 + +ldbcは[Typelevel](http://typelevel.org/)プロジェクトです。これは、Scalaの[行動規範](http://scala-lang.org/conduct.html)に記載されているように、純粋で、型にはまらない、関数型プログラミングを受け入れ、教育、学習、貢献のための安全でフレンドリーな環境を提供することを意味します。 ## はじめに @@ -21,76 +25,29 @@ - 関数型DSL (Slick, quill, zio-sql) - SQL文字列インターポレーター (Anorm, doobie) -LDBCも同じくJDBCをラップしたライブラリであり、LDBCはそれぞれの側面を組み合わせたScala 3ライブラリで、型安全でリファクタブルなSQLインターフェイスを提供し、MySQLのデータベース上でのSQL式を表現できます。 - -また、LDBCのコンセプトは、LDBCを使用することで単一リソースを管理することでScalaのモデルやsqlのスキーマ、ドキュメントを一元化できる開発を行えることです。 - -このコンセプトは宣言的でタイプセーフなWebエンドポイントライブラリである[tapir](https://github.com/softwaremill/tapir)から影響を受けました。
tapirを使用することで、型安全なエンドポイントを構築することができ、構築したエンドポイントからOpenAPIドキュメントを生成することもできます。 - -LDBCはデータベース層でScalaを使用して、同じように型安全な構築を可能にし、構築されたものを使用してドキュメントの生成を行えるようにします。 - -## なぜLDBCなのか? - -データベースを利用したアプリケーション開発では、様々な変更を継続的に行う必要があります。 - -例えば、データベースに構築されたテーブルのどの情報をアプリケーションで扱うべきか、データ検索にはどのようなクエリが最適か、などである。 - -テーブル定義にカラムを1つ追加するだけでも、SQLファイルの修正、対応するモデルへのプロパティの追加、データベースへの反映、ドキュメントの更新などが必要になります。 - -他にも考慮すべきこと、修正すべきことなどたくさんあります。 +ldbcも同じくJDBCをラップしたライブラリであり、ldbcはそれぞれの側面を組み合わせたScala 3ライブラリで、型安全でリファクタブルなSQLインターフェイスを提供し、MySQLのデータベース上でのSQL式を表現できます。 -日々の開発の中で全てをメンテナンスし続けるのはとても大変なことであり、メンテナンス漏れだって起こるかもしれません。 +ldbcは他のライブラリとは異なり、Scalaで構築された独自のコネクターも提供しています。 -テーブル情報をアプリケーション・モデルにマッピングすることなく、プレーンなSQLでデータを取得し、データを取得する際には指定された型で取得するというアプローチは非常に良い方法だと思います。 +Scalaは現在JVM, JS, Nativeというマルチプラットフォームに対応しています。 -この方法であれば、データベース固有のモデルを構築する必要がなく、開発者はデータを取得したいときに、取得したい種類のデータを使って自由にデータを扱うことができるからです。
また、プレーンなクエリを扱うことで、どのようなクエリが実行されるかを瞬時に把握できる点も非常に優れていると思います。 +しかし、JDBCを使用したライブラリだとJVM環境でしか動作しません。
+そのためldbcは、MySQLプロトコルに対応したScalaで書かれたコネクタを提供することで、異なるプラットフォームで動作できるようにするために開発を行っています。
+ldbcを使用することで、Scalaの型安全性と関数型プログラミングの利点を活かしながら、プラットフォームを問わずにデータベースアクセスを行うことができます。 -しかし、この方法ではテーブル情報のアプケーションでの管理がなくなっただけでドキュメントの更新などを解消することはできません。 +また、ldbcを使用することで単一リソースを管理することでScalaのモデルやsqlのスキーマ、ドキュメントを一元化できる開発を行えることです。 -LDBCは、これらの問題のいくつかを解決するために開発されています。 +このコンセプトは宣言的でタイプセーフなWebエンドポイントライブラリである[tapir](https://github.com/softwaremill/tapir)から影響を受けました。
tapirを使用することで型安全なエンドポイントを構築することができ、構築したエンドポイントからOpenAPIドキュメントを生成することもできます。 -- 型安全性:コンパイル時の保証、開発時の補完、読み取り時の情報 -- 宣言型:テーブル定義の形("What")とデータベース接続("How")を分離する。 -- SchemaSPYの統合:テーブル記述からドキュメントを生成する -- フレームワークではなくライブラリ: あなたのスタックに統合できる +ldbcはデータベース層でScalaを使用して、同じように型安全な構築を可能にし、構築されたものを使用してドキュメントの生成を行えるようにします。 -LDBCを使用するとデータベースの情報をアプリケーションで管理しなければいけませんが、型安全性とクエリの構築、ドキュメントの管理を一元化することができます。 +### 対象読者 -LDBCでのモデルをテーブル定義にマッピングするのはとても簡単です。 +このドキュメントは、Scalaプログラミング言語を使用してデータベースアクセスを行うためのライブラリであるldbcを使用する開発者を対象としています。 -モデルが持つプロパティと、そのカラムのために定義されるデータ型の間のマッピングも非常にシンプルです。開発者は、モデルが持つプロパティと同じ順序で、対応するカラムを定義するだけです。 - -```scala mdoc:silent -import ldbc.core.* - -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) -``` - -また、間違った型を組み合わせようとするとコンパイルエラーになります。 - -例えば、Userが持つString型のnameプロパティに関連するカラムにINT型のカラムを渡すとエラーになります。 - -```shell -[error] -- [E007] Type Mismatch Error: -[error] 169 | column("name", INT), -[error] | ^^^ -[error] |Found: ldbc.core.DataType.Integer[T] -[error] |Required: ldbc.core.DataType[String] -[error] | -[error] |where: T is a type variable with constraint <: Int | Long | Option[Int | Long] -``` +ldbcは、型付けされた純粋な関数型プログラミングに興味がある人のために設計されています。もしあなたがCatsユーザーでなかったり、関数型I/OやモナドCats Effectに馴染みがなかったりする場合は、ゆっくり進める必要があるかもしれません。 -これらのアドオンの詳細については、[テーブル定義](/ldbc/ja/01-Table-Definitions.html) を参照してください。 +とはいえ、もしこのドキュメントやldbc APIに戸惑ったり苛立ったりしたら、issueを発行して助けを求めてください。ライブラリもドキュメントも歴史が浅く、急速に変化しているため、不明瞭な点があるのは避けられません。従って、本書は問題や脱落に対処するために継続的に更新されます。 ## クイックスタート @@ -101,12 +58,15 @@ val table = Table[User]("user")( libraryDependencies ++= Seq( // まずはこの1つから - "$org$" %% "ldbc-core" % "$version$", + "$org$" %% "ldbc-dsl" % "$version$", + + // 使用するコネクタを選択 + "$org$" %% "jdbc-connector" % "$version$", // Javaコネクタ (対応プラットフォーム: JVM) + "$org$" %% "ldbc-connector" % "$version$", // Scalaコネクタ (対応プラットフォーム: JVM, JS, Native) // そして、必要に応じてこれらを加える - "$org$" %% "ldbc-dsl" % "$version$", // プレーンクエリー データベース接続 "$org$" %% "ldbc-query-builder" % "$version$", // 型安全なクエリ構築 - "$org$" %% "ldbc-schemaspy" % "$version$", // SchemaSPYドキュメント生成 + "$org$" %% "ldbc-schema" % "$version$", // データベーススキーマの構築 ) ``` @@@ @@ -122,6 +82,5 @@ sbtプラグインの使い方については、こちらの[documentation](/ldb - MySQL以外のデータベースサポート - ストリーミングのサポート - ZIOモジュールのサポート -- 他データベースライブラリとの統合 - テストキット - etc... From 3958d509b51f1a477f3398b7b131ef836c25bde3 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 3 Jul 2024 21:47:14 +0900 Subject: [PATCH 002/160] Create 01-Program --- docs/src/main/scala/01-Program.scala | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/src/main/scala/01-Program.scala diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala new file mode 100644 index 000000000..874a7098a --- /dev/null +++ b/docs/src/main/scala/01-Program.scala @@ -0,0 +1,38 @@ +import cats.syntax.all.* + +import cats.effect.* +import cats.effect.unsafe.implicits.global + +import org.typelevel.otel4s.trace.Tracer + +import ldbc.connector.* +import ldbc.dsl.Executor +import ldbc.dsl.io.* +import ldbc.dsl.logging.LogHandler + +@main def program1(): Unit = + // #given + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] + // #given + + // #program + val program: Executor[IO, Int] = Executor.pure[IO, Int](1) + // #program + + // #connection + def connection = Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password"), + ssl = SSL.None + ) + // #connection + + // #run + connection.use { conn => + program.readOnly(conn) + }.unsafeRunSync() + // 1 + // #run From 07e473b916fbc0a642bdd5895cd3d8cefadd40b0 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 3 Jul 2024 21:47:21 +0900 Subject: [PATCH 003/160] Create 02-Program --- docs/src/main/scala/02-Program.scala | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/src/main/scala/02-Program.scala diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala new file mode 100644 index 000000000..d16021d62 --- /dev/null +++ b/docs/src/main/scala/02-Program.scala @@ -0,0 +1,39 @@ +import cats.syntax.all.* + +import cats.effect.* +import cats.effect.unsafe.implicits.global + +import org.typelevel.otel4s.trace.Tracer + +import ldbc.connector.* +import ldbc.dsl.Executor +import ldbc.dsl.io.* +import ldbc.dsl.logging.LogHandler + +@main def program2(): Unit = + + // #given + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] + // #given + + // #program + val program: Executor[IO, Option[Int]] = sql"SELECT 2".query[Int].to[Option] + // #program + + // #connection + def connection = Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password"), + ssl = SSL.None + ) + // #connection + + // #run + connection.use { conn => + program.readOnly(conn) + }.unsafeRunSync() + // Some(2) + // #run From 3ad5e526e672146d13efb482364f24bca2c72416 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 3 Jul 2024 21:47:28 +0900 Subject: [PATCH 004/160] Create 03-Program --- docs/src/main/scala/03-Program.scala | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/src/main/scala/03-Program.scala diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala new file mode 100644 index 000000000..fbab1fb23 --- /dev/null +++ b/docs/src/main/scala/03-Program.scala @@ -0,0 +1,44 @@ +import cats.syntax.all.* + +import cats.effect.* +import cats.effect.unsafe.implicits.global + +import org.typelevel.otel4s.trace.Tracer + +import ldbc.connector.* +import ldbc.dsl.Executor +import ldbc.dsl.io.* +import ldbc.dsl.logging.LogHandler + +@main def program3(): Unit = + + // #given + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] + // #given + + // #program + val program: Executor[IO, (List[Int], Option[Int], Int)] = + for + result1 <- sql"SELECT 1".query[Int].to[List] + result2 <- sql"SELECT 2".query[Int].to[Option] + result3 <- sql"SELECT 3".query[Int].unsafe + yield (result1, result2, result3) + // #program + + // #connection + def connection = Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password"), + ssl = SSL.None + ) + // #connection + + // #run + connection.use { conn => + program.readOnly(conn) + }.unsafeRunSync() + // (List(1), Some(2), 3) + // #run From 19b8b4c05684e07c58078b12e32be185b8505e5b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 4 Jul 2024 00:00:06 +0900 Subject: [PATCH 005/160] Create 00-Setup --- docs/src/main/scala/00-Setup.scala | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/src/main/scala/00-Setup.scala diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala new file mode 100644 index 000000000..8f0b2c791 --- /dev/null +++ b/docs/src/main/scala/00-Setup.scala @@ -0,0 +1,46 @@ +import cats.syntax.all.* + +import cats.effect.* +import cats.effect.unsafe.implicits.global + +import org.typelevel.otel4s.trace.Tracer + +import ldbc.connector.* +import ldbc.dsl.Executor +import ldbc.dsl.io.* +import ldbc.dsl.logging.LogHandler + +@main def setup(): Unit = + + // #given + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] + // #given + + val createDatabase: Executor[IO, Int] = + sql"CREATE DATABASE IF NOT EXISTS todo".update + + val createTable: Executor[IO, Int] = + sql""" + CREATE TABLE `task` ( + `id` INT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `done` BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (`id`) + ) + """.update + + // #connection + def connection = Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password") + ) + // #connection + + // #run + connection.use { conn => + (createDatabase *> conn.setSchema("todo") *> createTable).transaction(conn) + }.unsafeRunSync() + // #run From 3ad04c53b6f40f093df68267bfcaa40c95e68858 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 4 Jul 2024 00:01:10 +0900 Subject: [PATCH 006/160] Fixed docs dependsOn package --- build.sbt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index e327db658..1fbd63595 100644 --- a/build.sbt +++ b/build.sbt @@ -214,14 +214,8 @@ lazy val docs = (project in file("docs")) ) .settings(commonSettings) .dependsOn( - core.jvm, - sql.jvm, - dsl.jvm, - queryBuilder.jvm, - schema.jvm, - schemaSpy, - codegen.jvm, - hikari + connector.jvm, + schema.jvm ) .enablePlugins(MdocPlugin, SitePreviewPlugin, ParadoxSitePlugin, GhpagesPlugin, NoPublishPlugin) From 78d6de614e6f5776b64bf8a29094441ba1c69d90 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 4 Jul 2024 00:01:29 +0900 Subject: [PATCH 007/160] Create 04-Program --- docs/src/main/scala/04-Program.scala | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/src/main/scala/04-Program.scala diff --git a/docs/src/main/scala/04-Program.scala b/docs/src/main/scala/04-Program.scala new file mode 100644 index 000000000..d651d4775 --- /dev/null +++ b/docs/src/main/scala/04-Program.scala @@ -0,0 +1,39 @@ +import cats.syntax.all.* + +import cats.effect.* +import cats.effect.unsafe.implicits.global + +import org.typelevel.otel4s.trace.Tracer + +import ldbc.connector.* +import ldbc.dsl.Executor +import ldbc.dsl.io.* +import ldbc.dsl.logging.LogHandler + +@main def program4(): Unit = + + // #given + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] + // #given + + // #program + val program: Executor[IO, Int] = + sql"INSERT INTO task (name, done) VALUES ('task 1', false)".update + // #program + + // #connection + def connection = Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password") + ) + // #connection + + // #run + connection.use { conn => + program.commit(conn) + }.unsafeRunSync() + // (List(1), Some(2), 3) + // #run From ec1efd0974e20fc83b638c1a56c4c9478592e63b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 4 Jul 2024 00:02:02 +0900 Subject: [PATCH 008/160] Fixed Program code --- docs/src/main/scala/01-Program.scala | 3 +-- docs/src/main/scala/02-Program.scala | 3 +-- docs/src/main/scala/03-Program.scala | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala index 874a7098a..62d5c03c6 100644 --- a/docs/src/main/scala/01-Program.scala +++ b/docs/src/main/scala/01-Program.scala @@ -25,8 +25,7 @@ import ldbc.dsl.logging.LogHandler host = "127.0.0.1", port = 3306, user = "ldbc", - password = Some("password"), - ssl = SSL.None + password = Some("password") ) // #connection diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala index d16021d62..5a8d3682b 100644 --- a/docs/src/main/scala/02-Program.scala +++ b/docs/src/main/scala/02-Program.scala @@ -26,8 +26,7 @@ import ldbc.dsl.logging.LogHandler host = "127.0.0.1", port = 3306, user = "ldbc", - password = Some("password"), - ssl = SSL.None + password = Some("password") ) // #connection diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala index fbab1fb23..d7f28d8c1 100644 --- a/docs/src/main/scala/03-Program.scala +++ b/docs/src/main/scala/03-Program.scala @@ -31,8 +31,7 @@ import ldbc.dsl.logging.LogHandler host = "127.0.0.1", port = 3306, user = "ldbc", - password = Some("password"), - ssl = SSL.None + password = Some("password") ) // #connection From 57d7df48418d35d7be38da6b918bae316b1ab26b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:20:05 +0900 Subject: [PATCH 009/160] Create 01-Migration-Notes document --- docs/src/main/mdoc/ja/01-Migration-Notes.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/src/main/mdoc/ja/01-Migration-Notes.md diff --git a/docs/src/main/mdoc/ja/01-Migration-Notes.md b/docs/src/main/mdoc/ja/01-Migration-Notes.md new file mode 100644 index 000000000..923caaefa --- /dev/null +++ b/docs/src/main/mdoc/ja/01-Migration-Notes.md @@ -0,0 +1,3 @@ +# 移行ノート + +## Upgrading to 0.2.x from 0.3.x From 027ab3caf16ff728dc0df339b397bc2ee44c5225 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:20:24 +0900 Subject: [PATCH 010/160] Create 02-Connection document --- docs/src/main/mdoc/ja/02-Connection.md | 94 ++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/src/main/mdoc/ja/02-Connection.md diff --git a/docs/src/main/mdoc/ja/02-Connection.md b/docs/src/main/mdoc/ja/02-Connection.md new file mode 100644 index 000000000..b569e5002 --- /dev/null +++ b/docs/src/main/mdoc/ja/02-Connection.md @@ -0,0 +1,94 @@ +# コネクション + +この章では、データベースに接続するためのコネクション構築方法について説明します。 + +データベースに接続するためには、コネクションを構築する必要がある。コネクションは、データベースへの接続を管理するためのリソースである。コネクションは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 + +ldbcはjdbcとldbc独自のコネクタのどちらかを使ってデータベースに接続する。どちらを使うかは設定する依存関係によって決まる。 + +## Use jdbc connector + +まず、`build.sbt`に依存関係を追加します。 + +jdbcコネクタを使用する場合、MySQLのコネクタも追加する必要があります。 + +@@@ vars +```scala +libraryDependencies ++= Seq( + "$org$" %% "jdbc-connector" % "$version$", + "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" +) +``` +@@@ + +次に、`MysqlDataSource`を使用してデータソースを作成します。 + +```scala +val ds = new com.mysql.cj.jdbc.MysqlDataSource() +ds.setServerName("127.0.0.1") +ds.setPortNumber(13306) +ds.setDatabaseName("world") +ds.setUser("ldbc") +ds.setPassword("password") +``` + +作成したデータソースを使用してjdbcコネクタのデータソースを作成します。 + +```scala +val datasource = jdbc.connector.MysqlDataSource[IO](ds) +``` + +最後に、jdbcコネクタを使用してコネクションを作成します。 + +```scala +val connection: Resource[IO, Connection[IO]] = + Resource.make(datasource.getConnection)(_.close()) +``` + +ここではCats Effectの`Resource`を使用してコネクション使用後にクローズするようにしています。 + +## Use ldbc connector + +まず、`build.sbt`に依存関係を追加します。 + +@@@ vars +```scala +libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" +``` +@@@ + +次に、Tracerを提供します。ldbcコネクタはTracerを使用してテレメトリデータの収集を行います。 これらは、アプリケーショントレースを記録するために使用されます。 + +ここでは、`Tracer.noop`を使用してTracerを提供します。 + +```scala +given Tracer[IO] = Tracer.noop[IO] +``` + +最後に、`Connection`を作成します。 + +```scala +val connection: Resource[IO, Connection[IO]] = + ldbc.connector.Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password"), + database = Some("ldbc") + ) +``` + +コネクションを設定するためのパラメータは以下の通りです。 + +| プロパティ | 詳細 | 必須 | +|--------------------------|--------------------------------------------------------------|----| +| host | データベースホスト情報 | ✅ | +| port | データベースポート情報 | ✅ | +| user | データベースユーザー情報 | ✅ | +| password | データベースパスワード情報 (default: None) | ❌ | +| database | データベース名情報 (default: None) | ❌ | +| debug | デバッグ情報を表示するかどうか (default: false) | ✅ | +| ssl | SSLの設定 (default: SSL.None) | ✅ | +| socketOptions | TCP/ UDP ソケットのソケットオプションを指定する (default: defaultSocketOptions) | ✅ | +| readTimeout | タイムアウト時間を指定する (default: Duration.Inf) | ✅ | +| allowPublicKeyRetrieval | 公開鍵を取得するかどうか (default: false) | ✅ | From 19d0aae038ffbe5d8878eb650b8e45895667f5bd Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:20:43 +0900 Subject: [PATCH 011/160] Create 03-Connecting-to-a-Database document --- .../mdoc/ja/03-Connecting-to-a-Database.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md diff --git a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md new file mode 100644 index 000000000..f1910b331 --- /dev/null +++ b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md @@ -0,0 +1,117 @@ +# データベースへの接続 + +この章では最初から始めます。まず、データベースに接続して値を返すプログラムを書き、そのプログラムをREPLで実行する。また、小さなプログラムを組み合わせてより大きなプログラムを構築することにも触れます。 + +## セットアップ + +まず、`build.sbt`に依存関係を追加します。 + +@@@ vars +```scala +libraryDependencies += "$org$" %% "ldbc-dsl" % "$version$" +``` +@@@ + +## 最初のプログラム + +ldbcを使う前に、いくつかのシンボルをインポートする必要がある。ここでは便宜上、パッケージのインポートを使用する。これにより、高レベルAPIで作業する際に最もよく使用されるシンボルを得ることができる。 + +```scala +import ldbc.dsl.io.* +import ldbc.dsl.logging.LogHandler +``` + +Catsも連れてこよう。 + +```scala +import cats.syntax.all.* +import cats.effect.* +``` + +次に、トレーサーとログハンドラーを提供する。これらは、アプリケーションのログを記録するために使用される。トレーサーは、アプリケーションのトレースを記録するために使用される。ログハンドラーは、アプリケーションのログを記録するために使用される。 + +以下のコードは、トレーサーとログハンドラーを提供するがその実体は何もしない。 + +@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #given } + +ldbc高レベルAPIで扱う最も一般的な型はExecutor[F, A]という形式で、{java | ldbc}.sql.Connectionが利用可能なコンテキストで行われる計算を指定し、最終的にA型の値を生成します。 + +では、定数を返すだけのExecutorプログラムから始めてみよう。 + +@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #program } + +次に、データベースに接続するためのコネクタを作成する。コネクタは、データベースへの接続を管理するためのリソースである。コネクタは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 + +@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #connection } + +Executorは、データベースへの接続方法、接続の受け渡し方法、接続のクリーンアップ方法を知っているデータ型であり、この知識によってExecutorをIOへ変換し、実行可能なプログラムを得ることができる。具体的には、実行するとデータベースに接続し、単一のトランザクションを実行するIOが得られる。 + +@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #run } + +万歳!定数を計算できた。これはデータベースに仕事を依頼することはないので、あまり面白いものではないが、最初の一歩が完了です。 + +> Keep in mind that all the code in this book is pure except the calls to IO.unsafeRunSync, which is the “end of the world” operation that typically appears only at your application’s entry points. In the REPL we use it to force a computation to “happen”. + +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala +``` + +## 2つめのプログラム + +では、sql string interpolatorを使って、データベースに定数の計算を依頼する問い合わせを作成してみましょう。 + +@@snip [02-Program.scala](/docs/src/main/scala/02-Program.scala) { #program } + +最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 + +@@snip [02-Program.scala](/docs/src/main/scala/02-Program.scala) { #run } + +定数を計算するためにデータベースに接続した。かなり印象的だ。 + +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala +``` + +## 3つめのプログラム + +一つの取引で複数のことをしたい場合はどうすればいいのか?簡単だ!Executorはモナドなので、for内包を使って2つの小さなプログラムを1つの大きなプログラムにすることができる。 + +@@snip [03-Program.scala](/docs/src/main/scala/03-Program.scala) { #program } + +最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 + +@@snip [03-Program.scala](/docs/src/main/scala/03-Program.scala) { #run } + +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala +``` + +## 4つめのプログラム + +データベースに対して書き込みを行うプログラムを書いてみよう。ここでは、データベースに接続し、クエリを実行し、データを挿入する。 + +@@snip [04-Program.scala](/docs/src/main/scala/04-Program.scala) { #program } + +先ほどと異なる点は、`commit`メソッドを呼び出すことである。これにより、トランザクションがコミットされ、データベースにデータが挿入される。 + +@@snip [04-Program.scala](/docs/src/main/scala/04-Program.scala) { #run } + +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala +``` From e9a4281549b78a0509b1a897a245280d7e6262dc Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:20:58 +0900 Subject: [PATCH 012/160] Create 04-Parameterized-Queries document --- .../main/mdoc/ja/04-Parameterized-Queries.md | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/src/main/mdoc/ja/04-Parameterized-Queries.md diff --git a/docs/src/main/mdoc/ja/04-Parameterized-Queries.md b/docs/src/main/mdoc/ja/04-Parameterized-Queries.md new file mode 100644 index 000000000..4947392d5 --- /dev/null +++ b/docs/src/main/mdoc/ja/04-Parameterized-Queries.md @@ -0,0 +1,123 @@ +# パラメータ化されたクエリ + +この章では、パラメータ化されたクエリを構築する方法を学びます。 + +使用するテーブルは以下の通りです。 + +```sql +CREATE TABLE country ( + code character(3) NOT NULL, + name text NOT NULL, + population integer NOT NULL, + gnp numeric(10,2) + -- more columns, but we won't use them here +) +``` + +```scala +case class Country(code: String, name: String, population: Int, gnp: Option[Double]) +``` + +## パラメータの追加 + +まずは、パラメーターを持たないクエリを作成します。 + +```scala +sql"SELECT code, name, population, gnp FROM country".query[Country].to[List] +``` + +次にクエリをメソッドに組み込んで、ユーザーが指定する国コードと一致するデータのみを選択するパラメーターを追加してみましょう。文字列の補間を行うのと同じように、code引数を$codeとしてSQL文に挿入します。 + +```scala +val code = "JPN" + +sql"SELECT code, name, population, gnp FROM country WHERE code = $code".query[Country].to[List] +``` + +コネクションを使用してクエリを実行すると問題なく動作します。 + +```scala +connection.use { conn => + sql"SELECT code, name, population, gnp FROM country WHERE code = $code" + .query[Country] + .to[List] + .readOnly(conn) +} +``` + +ここでは何が起こっているのでしょうか?文字列リテラルをSQL文字列にドロップしているだけのように見えますが、実際にはPreparedStatementを構築しており、code値は最終的にsetStringの呼び出しによって設定されます + +## 複数のパラメータ + +複数のパラメータも同じように機能する。驚きはない。 + +```scala +val code = "JPN" +val population = 100000000 + +connection.use { conn => + sql"SELECT code, name, population, gnp FROM country WHERE code = $code AND population > $population" + .query[Country] + .to[List] + .readOnly(conn) +} +``` + +## IN句の扱い + +SQLリテラルを扱う際によくあるイラつきは、一連の引数をIN句にインライン化したいという欲求ですが、SQLはこの概念をサポートしていません(JDBCも何もサポートしていません)。 + +```scala +val codes = NonEmptyList.of("JPN", "USA", "FRA") + +connection.use { conn => + sql"SELECT code, name, population, gnp FROM country WHERE" ++ in("code", codes) + .query[Country] + .to[List] + .readOnly(conn) +} +``` + +IN句は空であってはならないので、コードはNonEmptyListであることに注意。 + +このクエリーを実行すると、望ましい結果が得られる + +ldbcでは他にもいくつかの便利な関数が用意されています。 + +- `values` - VALUES句を生成する +- `in` - IN句を生成する +- `notIn` - NOT IN句を生成する +- `and` - AND句を生成する +- `or` - OR句を生成する +- `whereAnd` - AND句で括られた複数の条件のWHERE句を生成する +- `whereOr` - OR句で括られた複数の条件のWHERE句を生成する +- `set` - SET句を生成する +- `orderBy` - ORDER BY句を生成する + +## 静的なパラメーター + +パラメーターは動的ではありますが、時にはパラメーターとして使用しても静的な値として扱いたいことがあるかと思います。 + +例えば受け取った値に応じて取得するカラムを変更する場合、以下のように記述できます。 + +```scala +val column = "code" + +sql"SELECT $column FROM country".query[String].to[List] +``` + +動的なパラメーターはPreparedStatementによって処理が行われるため、クエリ文字列自体は`?`で置き換えられます。 + +そのため、このクエリは`SELECT ? FROM country`として実行されます。 + +これではログに出力されるクエリがわかりにくいため、`$column`は静的な値として扱いたい場合は、`$column`を`${sc(column)}`とすることで、クエリ文字列に直接埋め込まれるようになります。 + +```scala +val column = "code" + +sql"SELECT ${sc(column)} FROM country".query[String].to[List] +``` + +このクエリは`SELECT code FROM country`として実行されます。 + +> `sc(...)`は渡された文字列のエスケープを行わないことに注意してください。ユーザから与えられたデータを渡すことは、インジェクションのリスクになります。 From b98332cd3731c9ff28d63f4e8323cb477d53a673 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:21:28 +0900 Subject: [PATCH 013/160] Create 05-Selecting-Data.md document --- docs/src/main/mdoc/ja/05-Selecting-Data.md | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/src/main/mdoc/ja/05-Selecting-Data.md diff --git a/docs/src/main/mdoc/ja/05-Selecting-Data.md b/docs/src/main/mdoc/ja/05-Selecting-Data.md new file mode 100644 index 000000000..620d30d62 --- /dev/null +++ b/docs/src/main/mdoc/ja/05-Selecting-Data.md @@ -0,0 +1,80 @@ +# データ選択 + +この章では、ldbcデータセットを使用してデータを選択する方法を説明します。まず、データベースをセットアップします。以下のコードを使用して、MySQLデータベースをセットアップします。 + +@@@ vars +```yaml +version: '3' +services: + mysql: + image: mysql:"$mysqlVersion$" + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` +@@@ + +次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +``` + +## コレクションへの行の読み込み + +最初のクエリでは、低レベルのクエリを目指して、いくつかの国名をリストに選択し、最初の数件をプリントアウトしてみましょう。ここにはいくつかのステップがあるので、途中のタイプを記しておきます。 + +```scala +sql"SELECT name FROM country" + .query[String] // Query[IO, String] + .to[List] // Executor[IO, List[String]] + .readOnly(conn) // IO[List[String]] + .unsafeRunSync() // List[String] + .foreach(println) // Unit +``` + +これを少し分解してみよう。 + +- `sql"SELECT name FROM country".query[String]`は`Query[IO, String]`を定義し、返される各行をStringにマップする1列のクエリです。このクエリは1列のクエリで、返される行をそれぞれStringにマップします。 +- `.to[List]`は、行をリストに蓄積する便利なメソッドで、この場合は`Executor[IO, List[String]]`を生成します。このメソッドは、CanBuildFromを持つすべてのコレクション・タイプで動作します。 +- `readOnly(conn)`は`IO[List[String]]`を生成し、これを実行すると通常のScala List[String]が出力される。 +- `unsafeRunSync()`は、IOモナドを実行し、結果を取得する。これは、IOモナドを実行し、結果を取得するために使用される。 +- `foreach(println)`は、リストの各要素をプリントアウトする。 + +## 複数列クエリ + +もちろん、複数のカラムを選択してタプルにマッピングすることもできます。 + +```scala +sql"SELECT name, population FROM country" + .query[(String, Int)] // Query[IO, (String, Int)] + .to[List] // Executor[IO, List[(String, Int)]] + .readOnly(conn) // IO[List[(String, Int)]] + .unsafeRunSync() // List[(String, Int)] + .foreach(println) // Unit +``` + +ldbcは、複数のカラムを選択してクラスにマッピングすることもできます。 + +```scala +case class Country(name: String, population: Int) + +sql"SELECT name, population FROM country" + .query[Country] // Query[IO, Country] + .to[List] // Executor[IO, List[Country]] + .readOnly(conn) // IO[List[Country]] + .unsafeRunSync() // List[Country] + .foreach(println) // Unit +``` From 189cf33ebdcc6b6ef7595b1e970537f594dadf53 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:21:48 +0900 Subject: [PATCH 014/160] Create 07-Error-Handling document --- docs/src/main/mdoc/ja/06-Updating-Data.md | 141 ++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/src/main/mdoc/ja/06-Updating-Data.md diff --git a/docs/src/main/mdoc/ja/06-Updating-Data.md b/docs/src/main/mdoc/ja/06-Updating-Data.md new file mode 100644 index 000000000..33949e3b7 --- /dev/null +++ b/docs/src/main/mdoc/ja/06-Updating-Data.md @@ -0,0 +1,141 @@ +# データ更新 + +この章では、データベースのデータを変更する操作と、更新結果を取得する方法について説明します。 + +@@@ vars +```yaml +version: '3' +services: + mysql: + image: mysql:"$mysqlVersion$" + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` +@@@ + +次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +``` + +## 挿入 + +挿入は簡単で、selectと同様に動作します。ここでは、`task`テーブルに行を挿入するExecutorを作成するメソッドを定義します。 + +```scala +def insertTask(name: String, done: Boolean): Executor[IO, Int] = + sql"INSERT INTO task (name, done) VALUES ($name, $done)" + .update // Executor[IO, Int] +``` + +いくつかの行を挿入してみよう。 + +```scala +insertTask("task1", done = false).commit.unsafeRunSync() +insertTask("task2", done = true).commit.unsafeRunSync() +insertTask("task3", done = false).commit.unsafeRunSync() +``` + +そして読み返す。 + +```scala +sql"SELECT * FROM task" + .query[(Int, String, Boolean)] // Query[IO, (Int, String, Boolean)] + .to[List] // Executor[IO, List[(Int, String, Boolean)]] + .readOnly(conn) // IO[List[(Int, String, Boolean)]] + .unsafeRunSync() // List[(Int, String, Boolean)] + .foreach(println) // Unit +// (1,task1,false) +// (2,task2,true) +// (3,task3,false) +``` + +## 更新 + +更新も同じパターンだ。ここではタスクを完了済みに更新する。 + +```scala +def updateTaskDone(id: Int): Executor[IO, Int] = + sql"UPDATE task SET done = ${true} WHERE id = $id" + .update // Executor[IO, Int] +``` + +結果の取得 + +```scala +updateTaskDone(1).commit.unsafeRunSync() + +sql"SELECT * FROM task WHERE id = 1" + .query[(Int, String, Boolean)] // Query[IO, (Int, String, Boolean)] + .to[Option] // Executor[IO, List[(Int, String, Boolean)]] + .readOnly(conn) // IO[List[(Int, String, Boolean)]] + .unsafeRunSync() // List[(Int, String, Boolean)] + .foreach(println) // Unit +// Some((1,task1,true)) +``` + +## 自動生成キー + +インサートする際には、新しく生成されたキーを返したいものです。まず、挿入して最後に生成されたキーを`LAST_INSERT_ID`で取得し、指定された行を選択するという難しい方法をとります。 + +```scala +def insertTask(name: String, done: Boolean): Executor[IO, (Int, String, Boolean)] = + for { + _ <- sql"INSERT INTO task (name, done) VALUES ($name, $done)".update + id <- sql"SELECT LAST_INSERT_ID()".query[Int].unsafe + task <- sql"SELECT * FROM task WHERE id = $id".query[(Int, String, Boolean)].to[Option] + } yield task +``` + +```scala +insertTask("task4", done = false).commit.unsafeRunSync() +``` + +これは苛立たしいことだが、すべてのデータベースでサポートされている(ただし、「最後に使用されたIDを取得する」機能はベンダーによって異なる)。 + +MySQLでは、`AUTO_INCREMENT`が設定された行のみが挿入時に返すことができます。上記の操作を2つのステートメントに減らすことができます + +自動生成キーを使用して行を挿入する場合、`returning`メソッドを使用して自動生成キーを取得できます。 + +```scala +def insertTask(name: String, done: Boolean): Executor[IO, (Int, String, Boolean)] = + for { + id <- sql"INSERT INTO task (name, done) VALUES ($name, $done)".returning[Long] + task <- sql"SELECT * FROM task WHERE id = $id".query[(Int, String, Boolean)].to[Option] + } yield task +``` + +```scala +insertTask("task5", done = false).commit.unsafeRunSync() +``` + +## バッチ更新 + +バッチ更新を行うには、`NonEmptyList`を使用して複数の行を挿入する`insertManyTask`メソッドを定義します。 + +```scala +def insertManyTask(tasks: NonEmptyList[(String, Boolean)]): Executor[IO, Int] = { + val value = tasks.map { case (name, done) => sql"($name, $done)" } + (sql"INSERT INTO task (name, done) VALUES" ++ values(value)).update +} +``` + +このプログラムを実行すると、更新された行数が得られる。 + +```scala +insertManyTask(NonEmptyList.of(("task6", false), ("task7", true), ("task8", false))).commit.unsafeRunSync() +``` From b0b789b4ae4ff42827d6a1524a676b7af4a2a61c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:22:05 +0900 Subject: [PATCH 015/160] Create 07-Error-Handling document --- docs/src/main/mdoc/ja/07-Error-Handling.md | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/src/main/mdoc/ja/07-Error-Handling.md diff --git a/docs/src/main/mdoc/ja/07-Error-Handling.md b/docs/src/main/mdoc/ja/07-Error-Handling.md new file mode 100644 index 000000000..b7b4aced4 --- /dev/null +++ b/docs/src/main/mdoc/ja/07-Error-Handling.md @@ -0,0 +1,44 @@ +# エラーハンドリング + +この章では、例外をトラップしたり処理したりするプログラムを構築するためのコンビネーター一式を検討する。 + +@@@ vars +```yaml +version: '3' +services: + mysql: + image: mysql:"$mysqlVersion$" + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` +@@@ + +次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +``` + +## 例外について + +ある操作が成功するかどうかは、ネットワークの健全性、テーブルの現在の内容、ロックの状態など、予測できない要因に依存します。そのため、EitherT[Executor, Throwable, A]のような論理和ですべてを計算するか、明示的に捕捉されるまで例外の伝播を許可するかを決めなければならない。つまり、ldbcのアクション(ターゲット・モナドに変換される)が実行されると、例外が発生する可能性がある。 + +発生しやすい例外は主に3種類ある + +1. あらゆる種類のI/Oで様々なタイプのIOExceptionが発生する可能性があり、これらの例外は回復できない傾向がある。 +2. データベース例外は、通常、ベンダー固有のSQLStateで特定のエラーを識別する一般的なSQLExceptionとして、キー違反のような一般的な状況で発生します。エラーコードは伝承として伝えられるか、実験によって発見されなければなりません。XOPENとSQL:2003の標準がありますが、どのベンダーもこれらの仕様に忠実ではないようです。これらのエラーには回復可能なものとそうでないものがある。 +3. ldbcは、無効な型マッピング、ドライバから返される未知の JDBC 定数、観測される NULL 値、その他 ldbc が想定している不変条件の違反に対して InvariantViolation を発生させます。これらの例外はプログラマのエラーかドライバの不適合を示し、一般に回復不可能です。 + From 21b7bb1dea5a76286a3a21897b08e0206cf008d4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:22:22 +0900 Subject: [PATCH 016/160] Create 08-Logging document --- docs/src/main/mdoc/ja/08-Logging.md | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/src/main/mdoc/ja/08-Logging.md diff --git a/docs/src/main/mdoc/ja/08-Logging.md b/docs/src/main/mdoc/ja/08-Logging.md new file mode 100644 index 000000000..e3f61bf78 --- /dev/null +++ b/docs/src/main/mdoc/ja/08-Logging.md @@ -0,0 +1,47 @@ +# ログ + +ldbcではDatabase接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 + +標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 + +```scala 3 +given LogHandler[IO] = LogHandler.console[IO] +``` + +任意のロギングライブラリを使用してログをカスタマイズする場合は`ldbc.dsl.logging.LogHandler`を使用します。 + +以下は標準実装のログ実装です。ldbcではデータベース接続で以下3種類のイベントが発生します。 + +- Success: 処理の成功 +- ProcessingFailure: データ取得後もしくはデータベース接続前の処理のエラー +- ExecFailure: データベースへの接続処理のエラー + +それぞれのイベントでどのようなログを書き込むかをパターンマッチングによって振り分けを行います。 + +```scala 3 +def console[F[_]: Console: Sync]: LogHandler[F] = + case LogEvent.Success(sql, args) => + Console[F].println( + s"""Successful Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) + case LogEvent.ProcessingFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed ResultSet Processing: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) + case LogEvent.ExecFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) +``` From 540a0a16e830ad27ba6ab1649739622a4bdc19a9 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:22:39 +0900 Subject: [PATCH 017/160] Create 09-Custom-Data-Type document --- docs/src/main/mdoc/ja/09-Custom-Data-Type.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/src/main/mdoc/ja/09-Custom-Data-Type.md diff --git a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md new file mode 100644 index 000000000..6caea6014 --- /dev/null +++ b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md @@ -0,0 +1 @@ +# カスタム データ型 From 988bcce9eb670e84d099f3df795bb4043e8ab802 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:22:53 +0900 Subject: [PATCH 018/160] Create 10-Query-Builder document --- docs/src/main/mdoc/ja/10-Query-Builder.md | 420 ++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 docs/src/main/mdoc/ja/10-Query-Builder.md diff --git a/docs/src/main/mdoc/ja/10-Query-Builder.md b/docs/src/main/mdoc/ja/10-Query-Builder.md new file mode 100644 index 000000000..85caee6da --- /dev/null +++ b/docs/src/main/mdoc/ja/10-Query-Builder.md @@ -0,0 +1,420 @@ +# クエリビルダー + +この章では、型安全にクエリを構築するための方法について説明します。 + +プロジェクトに以下の依存関係を設定する必要があります。 + +@@@ vars +```scala +libraryDependencies += "$org$" %% "ldbc-query-builder" % "$version$" +``` +@@@ + +@@@ vars +```yaml +version: '3' +services: + mysql: + image: mysql:"$mysqlVersion$" + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` +@@@ + +次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +``` + +ldbcでは、クラスを使用してクエリを構築します。 + +```scala +import ldbc.query.builder.* + +case class Task(id: Int, name: String, done: Boolean) derives Table +``` + +`Task`クラスは`Table`トレイトを継承しています。`Table`トレイトは`Table`クラスを継承しているため、`Table`クラスのメソッドを使用してクエリを構築することができます。 + +```scala +val query = Table[Task] + .select(task => (task.id, task.name, task.done)) + .where(_.done === true) + .orderBy(_.id.asc) + .limit(1) +``` + +## SELECT + +型安全にSELECT文を構築する方法はTableが提供する`select`メソッドを使用することです。ldbcではプレーンなクエリに似せて実装されているため直感的にクエリ構築が行えます。またどのようなクエリが構築されているかも一目でわかるような作りになっています。 + +特定のカラムのみ取得を行うSELECT文を構築するには`select`メソッドで取得したいカラムを指定するだけです。 + +```scala +val select = Table[Task] + .select(_.id) + +select.statement === "SELECT id FROM task" +``` + +複数のカラムを指定する場合は`select`メソッドで取得したいカラムを指定して指定したカラムのタプルを返すだけです。 + +```scala +val select = Table[Task] + .select(task => (task.id, task.name)) + +select.statement === "SELECT id, name FROM task" +``` + +全てのカラムを指定したい場合はTableが提供する`selectAll`メソッドを使用することで構築できます。 + +```scala +val select = Table[Task] + .selectAll + +select.statement === "SELECT id, name, done FROM task" +``` + +特定のカラムの数を取得したい場合は、指定したカラムで`count`を使用することで構築できます。  + +```scala +val select = Table[Task] + .select(_.id.count) + +select.statement === "SELECT COUNT(id) FROM task" +``` + +### WHERE + +クエリに型安全にWhere条件を設定する方法は`where`メソッドを使用することです。 + +```scala +val where = Table[Task] + .where(_.done === true) + +where.statement === "SELECT id, name, done FROM task WHERE done = ?" +``` + +`where`メソッドで使用できる条件の一覧は以下です。 + +| 条件 | ステートメント | +|--------------------------------------|---------------------------------------| +| === | `column = ?` | +| >= | `column >= ?` | +| > | `column > ?` | +| <= | `column <= ?` | +| < | `column < ?` | +| <> | `column <> ?` | +| !== | `column != ?` | +| IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL") | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | +| <=> | `column <=> ?` | +| IN (value, value, ...) | `column IN (?, ?, ...)` | +| BETWEEN (start, end) | `column BETWEEN ? AND ?` | +| LIKE (value) | `column LIKE ?` | +| LIKE_ESCAPE (like, escape) | `column LIKE ? ESCAPE ?` | +| REGEXP (value) | `column REGEXP ?` | +| `<<` (value) | `column << ?` | +| `>>` (value) | `column >> ?` | +| DIV (cond, result) | `column DIV ? = ?` | +| MOD (cond, result) | `column MOD ? = ?` | +| ^ (value) | `column ^ ?` | +| ~ (value) | `~column = ?` | + +### GROUP BY/Having + +クエリに型安全にGROUP BY句を設定する方法は`groupBy`メソッドを使用することです。 + +`groupBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準にグループ化することができます。 + +```scala +val select = Table[Task] + .select(task => (task.id, task.name)) + .groupBy(_._2) + +select.statement === "SELECT id, name FROM task GROUP BY name" +``` + +グループ化すると`select`で取得できるデータの数はグループの数だけとなります。そこでグループ化を行った場合には、グループ化に指定したカラムの値や、用意された関数を使ってカラムの値をグループ単位で集計した結果などを取得することができます。 + +`having`を使用すると`groupBy`によってグループ化されて取得したデータに関して、取得する条件を設定することができます。 + +```scala +val select = Table[Task] + .select(task => (task.id, task.name)) + .groupBy(_._2) + .having(_._1 > 1) + +select.statement === "SELECT id, name FROM task GROUP BY name HAVING id > ?" +``` + +### ORDER BY + +クエリに型安全にORDER BY句を設定する方法は`orderBy`メソッドを使用することです。 + +`orderBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準に昇順、降順で並び替えることができます。 + +```scala +val select = Table[Task] + .select(task => (task.id, task.name)) + .orderBy(_.id) + +select.statement === "SELECT id, name FROM task ORDER BY id" +``` + +昇順/降順を指定したい場合は、それぞれカラムに対して `asc`/`desc`を呼び出すだけです。 + +```scala +val select = Table[Task] + .select(task => (task.id, task.name)) + .orderBy(_.id.asc) + +select.statement === "SELECT id, name FROM task ORDER BY id ASC" +``` + +### LIMIT/OFFSET + +クエリに型安全にLIMIT句とOFFSET句を設定する方法は`limit`/`offset`メソッドを使用することです。 + +`limit`を設定すると`select`を実行した時に取得するデータの行数の上限を設定することができ、`offset`を設定すると何番目からのデータを取得するのかを指定することができます。 + +```scala +val select = Table[Task] + .select(task => (task.id, task.name)) + .limit(1) + .offset(1) + +select.statement === "SELECT id, name FROM task LIMIT ? OFFSET ?" +``` + +## JOIN/LEFT JOIN/RIGHT JOIN + +クエリに型安全にJoinを設定する方法は`join`/`leftJoin`/`rightJoin`メソッドを使用することです。 + +Joinでは以下定義をサンプルとして使用します。 + +```scala +case class Country(code: String, name: String) derives Table +case class City(id: Int, name: String, countryCode: String) derives Table +case class CountryLanguage(countryCode: String, language: String) derives Table + +val countryTable = Table[Country] +val cityTable = Table[City] +val countryLanguageTable = Table[CountryLanguage] +``` + +まずシンプルなJoinを行いたい場合は、`join`を使用します。 +`join`の第一引数には結合したいテーブルを渡し、第二引数では結合元のテーブルと結合したいテーブルのカラムで比較を行う関数を渡します。これはJoinにおいてのON句に該当します。 + +Join後の`select`は2つのテーブルからカラムを指定することになります。 + +```scala +val join = countryTable.join(cityTable)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country JOIN city ON country.code = city.country_code" +``` + +次に左外部結合であるLeft Joinを行いたい場合は、`leftJoin`を使用します。 +`join`が`leftJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 + +```scala 3 +val leftJoin = countryTable.leftJoin(cityTable)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city ON country.code = city.country_code" +``` + +シンプルなJoinとの違いは`leftJoin`を使用した場合、結合を行うテーブルから取得するレコードはNULLになる可能性があるということです。 + +そのためldbcでは`leftJoin`に渡されたテーブルから取得するカラムのレコードは全てOption型になります。 + +```scala 3 +val leftJoin = countryTable.leftJoin(cityTable)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) // (String, Option[String]) +``` + +次に右外部結合であるRight Joinを行いたい場合は、`rightJoin`を使用します。 +こちらも`join`が`rightJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 + +```scala 3 +val rightJoin = countryTable.rightJoin(cityTable)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN city ON country.code = city.country_code" +``` + +シンプルなJoinとの違いは`rightJoin`を使用した場合、結合元のテーブルから取得するレコードはNULLになる可能性があるということです。 + +そのためldbcでは`rightJoin`を使用した結合元のテーブルから取得するカラムのレコードは全てOption型になります。 + +```scala 3 +val rightJoin = countryTable.rightJoin(cityTable)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) // (Option[String], String) +``` + +複数のJoinを行いたい場合は、メソッドチェーンで任意のJoinメソッドを呼ぶことで実現することができます。 + +```scala 3 +val join = + (countryTable join cityTable)((country, city) => country.code === city.countryCode) + .rightJoin(countryLanguageTable)((_, city, countryLanguage) => city.countryCode === countryLanguage.countryCode) + .select((country, city, countryLanguage) => (country.name, city.name, countryLanguage.language)) // (Option[String], Option[String], String)] + +join.statement = + """ + |SELECT + | country.`name`, + | city.`name`, + | country_language.`language` + |FROM country + |JOIN city ON country.code = city.country_code + |RIGHT JOIN country_language ON city.country_code = country_language.country_code + |""".stripMargin +``` + +複数のJoinを行っている状態で`rightJoin`での結合を行うと、今までの結合が何であったかにかかわらず直前まで結合していたテーブルから取得するレコードは全てNULL許容なアクセスとなることに注意してください。 + +## INSERT + +型安全にINSERT文を構築する方法はTableが提供する以下のメソッドを使用することです。 + +- insert +- insertInto +- += +- ++= + +**insert** + +`insert`メソッドには挿入するデータのタプルを渡します。タプルはモデルと同じプロパティの数と型である必要があります。また、挿入されるデータの順番はモデルのプロパティおよびテーブルのカラムと同じ順番である必要があります。 + +```scala 3 +val insert = task.insert((1L, "name", false)) + +insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?)" +``` + +複数のデータを挿入したい場合は、`insert`メソッドに複数のタプルを渡すことで構築できます。 + +```scala 3 +val insert = task.insert((1L, "name", false), (2L, "name", true)) + +insert.statement === "INSERT INTO task (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +**insertInto** + +`insert`メソッドはテーブルが持つ全てのカラムにデータ挿入を行いますが、特定のカラムに対してのみデータを挿入したい場合は`insertInto`メソッドを使用します。 + +これはAutoIncrementやDefault値を持つカラムへのデータ挿入を除外したい場合などに使用できます。 + +```scala 3 +val insert = task.insertInto(task => (task.name, task.done)).values(("name", false)) + +insert.statement === "INSERT INTO task (`name`, `done`) VALUES(?, ?)" +``` + +複数のデータを挿入したい場合は、`values`にタプルの配列を渡すことで構築できます。 + +```scala 3 +val insert = task.insertInto(task => (task.name, task.done)).values(List(("name", false), ("name", true))) + +insert.statement === "INSERT INTO task (`name`, `done`) VALUES(?, ?), (?, ?)" +``` + +**+=** + +`+=`メソッドを使用することでモデルを使用してinsert文を構築することができます。モデルを使用する場合は全てのカラムにデータを挿入してしまうことに注意してください。 + +```scala 3 +val insert = task += Task(1L, "name", false) + +insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?)" +``` + +**++=** + +モデルを使用して複数のデータを挿入したい場合は`++=`メソッドを使用します。 + +```scala 3 +val insert = task ++= List(Task(1L, "name", false), Task(2L, "name", true)) + +insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +### ON DUPLICATE KEY UPDATE + +ON DUPLICATE KEY UPDATE 句を指定し行を挿入すると、UNIQUEインデックスまたはPRIMARY KEYで値が重複する場合、古い行のUPDATEが発生します。 + +ldbcでこの処理を実現する方法は、`Insert`に対して`onDuplicateKeyUpdate`を使用することです。 + +```scala +val insert = task.insert((1L, "name", false)).onDuplicateKeyUpdate(v => (v.name, v.done)) + +insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?) AS new_task ON DUPLICATE KEY UPDATE `name` = new_task.`name`, `done` = new_task.`done`" +``` + +## UPDATE + +型安全にUPDATE文を構築する方法はTableが提供する`update`メソッドを使用することです。 + +`update`メソッドの第1引数にはテーブルのカラム名ではなくモデルのプロパティ名を指定し、第2引数に更新したい値を渡します。第2引数に渡す値の型は第1引数で指定したプロパティの型と同じである必要があります。 + +```scala +val update = task.update("name", "update name") + +update.statement === "UPDATE task SET name = ?" +``` + +第1引数に存在しないプロパティ名を指定した場合コンパイルエラーとなります。 + +```scala 3 +val update = task.update("hoge", "update name") // Compile error +``` + +複数のカラムを更新したい場合は`set`メソッドを使用します。 + +```scala 3 +val update = task.update("name", "update name").set("done", false) + +update.statement === "UPDATE task SET name = ?, done = ?" +``` + +`set`メソッドには条件に応じてクエリを生成させないようにすることもできます。 + +```scala 3 +val update = task.update("name", "update name").set("done", false, false) + +update.statement === "UPDATE task SET name = ?" +``` + +モデルを使用してupdate文を構築することもできます。モデルを使用する場合は全てのカラムを更新してしまうことに注意してください。 + +```scala 3 +val update = task.update(Task(1L, "update name", false)) + +update.statement === "UPDATE task SET id = ?, name = ?, done = ?" +``` + +## DELETE + +型安全にDELETE文を構築する方法はTableが提供する`delete`メソッドを使用することです。 + +```scala +val delete = task.delete + +delete.statement === "DELETE FROM task" +``` From 69a595e10873b4bc4af58ae3c477eb9814db0d27 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:23:07 +0900 Subject: [PATCH 019/160] Create 11-Schema document --- docs/src/main/mdoc/ja/11-Schema.md | 406 +++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 docs/src/main/mdoc/ja/11-Schema.md diff --git a/docs/src/main/mdoc/ja/11-Schema.md b/docs/src/main/mdoc/ja/11-Schema.md new file mode 100644 index 000000000..87a1748da --- /dev/null +++ b/docs/src/main/mdoc/ja/11-Schema.md @@ -0,0 +1,406 @@ +# スキーマ + +この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、[code generator](/ldbc/ja/07-Schema-Code-Generation.html) を使ってこの作業を省略することもできます。 + +以下のコード例では、以下のimportを想定しています。 + +```scala 3 +import ldbc.schema.* +import ldbc.schema.attribute.* +``` + +ldbcは、Scalaモデルとデータベースのテーブル定義を1対1のマッピングで管理します。モデルが保持するプロパティとテーブルが保持するカラムのマッピングは、定義順に行われます。テーブル定義は、Create文の構造と非常によく似ています。このため、テーブル定義の構築はユーザーにとって直感的なものとなります。 + +ldbc は、このテーブル定義をさまざまな目的で使用します。型安全なクエリの生成、ドキュメントの生成など。 + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( // CREATE TABLE `user` ( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, + column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL +) // ); +``` + +すべてのカラムはcolumnメソッドで定義されます。各カラムにはカラム名、データ型、属性があります。以下のプリミティブ型が標準でサポートされており、すぐに使用できます。 + +- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` +- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` +- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` +- String +- Boolean +- java.time.* + +Null可能な列はOption[T]で表現され、Tはサポートされるプリミティブ型の1つです。Option型でない列はすべてNot Nullであることに注意してください。 + +## データ型 + +モデルが持つプロパティのScala型とカラムが持つデータ型の対応付けは、定義されたデータ型がScala型をサポートしている必要があります。サポートされていない型を割り当てようとするとコンパイルエラーが発生します。 + +データ型がサポートするScalaの型は以下の表の通りです。 + +| Data Type | Scala Type | +|------------|-----------------------------------------------------------------------------------------------| +| BIT | Byte, Short, Int, Long | +| TINYINT | Byte, Short | +| SMALLINT | Short, Int | +| MEDIUMINT | Int | +| INT | Int, Long | +| BIGINT | Long, BigInt | +| DECIMAL | BigDecimal | +| FLOAT | Float | +| DOUBLE | Double | +| CHAR | String | +| VARCHAR | String | +| BINARY | Array[Byte] | +| VARBINARY | Array[Byte] | +| TINYBLOB | Array[Byte] | +| BLOB | Array[Byte] | +| MEDIUMBLOB | Array[Byte] | +| LONGBLOB | Array[Byte] | +| TINYTEXT | String | +| TEXT | String | +| MEDIUMTEXT | String | +| DATE | java.time.LocalDate | +| DATETIME | java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime | +| TIMESTAMP | java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime | +| TIME | java.time.LocalTime | +| YEAR | java.time.Instant, java.time.LocalDate, java.time.Year | +| BOOLEAN | Boolean | + +**整数型を扱う際の注意点** + +符号あり、符号なしに応じて、扱えるデータの範囲がScalaの型に収まらないことに注意する必要があります。 + +| Data Type | signed range | unsigned range | Scala Type | range | +|-----------|--------------------------------------------|--------------------------|----------------|--------------------------------------------------------------------| +| TINYINT | -128 ~ 127 | 0 ~ 255 | Byte
Short | -128 ~ 127
-32768~32767 | +| SMALLINT | -32768 ~ 32767 | 0 ~ 65535 | Short
Int | -32768~32767
-2147483648~2147483647 | +| MEDIUMINT | -8388608 ~ 8388607 | 0 ~ 16777215 | Int | -2147483648~2147483647 | +| INT | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | Int
Long | -2147483648~2147483647
-9223372036854775808~9223372036854775807 | +| BIGINT | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 18446744073709551615 | Long
BigInt | -9223372036854775808~9223372036854775807
... | + +ユーザー定義の独自型やサポートされていない型を扱う場合は、[カスタム型](/ldbc/ja/02-Custom-Data-Type.html) を参照してください。 + +## 属性 + +カラムにはさまざまな属性を割り当てることができます。 + +- `AUTO_INCREMENT` + DDL文を作成し、SchemaSPYを文書化する際に、列を自動インクリメント・キーとしてマークする。 + MySQLでは、データ挿入時にAutoIncでないカラムを返すことはできません。そのため、必要に応じて、ldbcは戻りカラムがAutoIncとして適切にマークされているかどうかを確認します。 +- `PRIMARY_KEY` + DDL文やSchemaSPYドキュメントを作成する際に、列を主キーとしてマークする。 +- `UNIQUE_KEY` + DDL文やSchemaSPYドキュメントを作成する際に、列を一意キーとしてマークする。 +- `COMMENT` + DDL文やSchemaSPY文書を作成する際に、列にコメントを設定する。 + +## キーの設定 + +MySQLではテーブルに対してUniqueキーやIndexキー、外部キーなどの様々なキーを設定することができます。ldbcで構築したテーブル定義でこれらのキーを設定する方法を見ていきましょう。 + +### PRIMARY KEY + +主キー(primary key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムにプライマリーキー制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。また NULL も格納することができません。その結果、プライマリーキー制約が設定されたカラムの値を検索することで、テーブルの中でただ一つのデータを特定することができます。 + +ldbcではこのプライマリーキー制約を2つの方法で設定することができます。 + +1. columnメソッドの属性として設定する +2. tableのkeySetメソッドで設定する + +**columnメソッドの属性として設定する** + +columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`PRIMARY_KEY`を渡すだけです。これによって以下の場合 `id`カラムを主キーとして設定することができます。 + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) +``` + +**tableのkeySetメソッドで設定する** + +ldbcのテーブル定義には `keySet`というメソッドが生えており、ここで`PRIMARY_KEY`に主キーとして設定したいカラムを渡すことで主キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => PRIMARY_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`) +// ) +``` + +`PRIMARY_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 + +- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH +- `Index Option` ldbc.schema.Index.IndexOption + +#### 複合キー (primary key) + +1つのカラムだけではなく、複数のカラムを主キーとして組み合わせ主キーとして設定することもできます。`PRIMARY_KEY`に主キーとして設定したいカラムを複数渡すだけで複合主キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => PRIMARY_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`, `name`) +// ) +``` + +複合キーは`keySet`メソッドでの`PRIMARY_KEY`でしか設定することはできません。仮に以下のようにcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを主キーとして設定されてしまいます。 + +ldbcではテーブル定義に複数`PRIMARY_KEY`を設定したとしてもコンパイルエラーにすることはできません。しかし、テーブル定義をクエリの生成やドキュメントの生成などで使用する場合エラーとなります。これはPRIMARY KEYはテーブルごとに1つしか設定することができないという制約によるものです。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255), PRIMARY_KEY), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + +// CREATE TABLE `user` ( +// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, +// ) +``` + +### UNIQUE KEY + +一意キー(unique key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムに一意性制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。 + +ldbcではこの一意性制約を2つの方法で設定することができます。 + +1. columnメソッドの属性として設定する +2. tableのkeySetメソッドで設定する + +**columnメソッドの属性として設定する** + +columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`UNIQUE_KEY`を渡すだけです。これによって以下の場合 `id`カラムを一意キーとして設定することができます。 + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) +``` + +**tableのkeySetメソッドで設定する** + +ldbcのテーブル定義には `keySet`というメソッドが生えており、ここで`UNIQUE_KEY`に一意キーとして設定したいカラムを渡すことで一意キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => UNIQUE_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`) +// ) +``` + +`UNIQUE_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 + +- `Index Name` String +- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH +- `Index Option` ldbc.schema.Index.IndexOption + +#### 複合キー (unique key) + +1つのカラムだけではなく、複数のカラムを一意キーとして組み合わせ一意キーとして設定することもできます。`UNIQUE_KEY`に一意キーとして設定したいカラムを複数渡すだけで複合一意キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => UNIQUE_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`, `name`) +// ) +``` + +複合キーは`keySet`メソッドでの`UNIQUE_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを一意キーとして設定されてしまいます。 + +### INDEX KEY + +インデックスキー(index key)とはMySQLにおいて目的のレコードを効率よく取得するための「索引」のことです。 + +ldbcではこのインデックスを2つの方法で設定することができます。 + +1. columnメソッドの属性として設定する +2. tableのkeySetメソッドで設定する + +**columnメソッドの属性として設定する** + +columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`INDEX_KEY`を渡すだけです。これによって以下の場合 `id`カラムをインデックスとして設定することができます。 + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) +``` + +**tableのkeySetメソッドで設定する** + +ldbcのテーブル定義には `keySet`というメソッドが生えており、ここで`INDEX_KEY`にインデックスとして設定したいカラムを渡すことでインデックスキーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => INDEX_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`) +// ) +``` + +`INDEX_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 + +- `Index Name` String +- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH +- `Index Option` ldbc.schema.Index.IndexOption + +#### 複合キー (index key) + +1つのカラムだけではなく、複数のカラムをインデックスキーとして組み合わせインデックスキーとして設定することもできます。`INDEX_KEY`にインデックスキーとして設定したいカラムを複数渡すだけで複合インデックスとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => INDEX_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`, `name`) +// ) +``` + +複合キーは`keySet`メソッドでの`INDEX_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合インデックスとしてではなく、それぞれをインデックスキーとして設定されてしまいます。 + +### FOREIGN KEY + +外部キー(foreign key)とは、MySQLにおいてデータの整合性を保つための制約(参照整合性制約)です。 外部キーに設定されているカラムには、参照先となるテーブルのカラム内に存在している値しか設定できません。 + +ldbcではこの外部キー制約をtableのkeySetメソッドを使用する方法で設定することができます。 + +```scala 3 +val post = Table[Post]("post")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)) +) + +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// ) +``` + +`FOREIGN_KEY`メソッドにはカラムとReference値意外にも以下のパラメーターを設定することができます。 + +- `Index Name` String + +外部キー制約には親テーブルの削除時と更新時の挙動を設定することができます。`REFERENCE`メソッドに`onDelete`と`onUpdate`メソッドが提供されているのでこちらを使用することでそれぞれ設定することができます。 + +設定することのできる値は`ldbc.schema.Reference.ReferenceOption`から取得することができます。 + +```scala 3 +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id).onDelete(Reference.ReferenceOption.RESTRICT))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT +// ) +``` + +設定することのできる値は以下になります。 + +- `RESTRICT`: 親テーブルに対する削除または更新操作を拒否します。 +- `CASCADE`: 親テーブルから行を削除または更新し、子テーブル内の一致する行を自動的に削除または更新します。 +- `SET_NULL`: 親テーブルから行を削除または更新し、子テーブルの外部キーカラムを NULL に設定します。 +- `NO_ACTION`: 標準 SQL のキーワード。 MySQLでは、RESTRICT と同等です。 +- `SET_DEFAULT`: このアクションは MySQL パーサーによって認識されますが、InnoDB と NDB はどちらも、ON DELETE SET DEFAULT または ON UPDATE SET DEFAULT 句を含むテーブル定義を拒否します。 + +#### 複合キー (foreign key) + +1つのカラムだけではなく、複数のカラムを外部キーとして組み合わせて設定することもできます。`FOREIGN_KEY`に外部キーとして設定したいカラムを複数渡すだけで複合外部キーとして設定することができます。 + +```scala 3 +val post = Table[Post]("post")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("category", SMALLINT[Short]) +) + +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]), + column("post_category", SMALLINT[Short]) +) + .keySet(table => FOREIGN_KEY((table.postId, table.postCategory), REFERENCE(post, (post.id, post.category)))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`, `post_category`) REFERENCES `post` (`id`, `category`) +// ) +``` + +### 制約名 + +MySQLではCONSTRAINTを使用することで制約に対して任意の名前を付与することができます。この制約名はデータベース単位で一意の値である必要があります。 + +ldbcではCONSTRAINTメソッドが提供されているのでキー制約などの制約を設定する処理をCONSTRAINTメソッドに渡すだけで設定することができます。 + +```scala 3 +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => CONSTRAINT("fk_post_id", FOREIGN_KEY(table.postId, REFERENCE(post, post.id)))) + +// CREATE TABLE `user` ( +// ..., +// CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// ) +``` From 3f1e10760687959b7f63488f2c62e9590dd2da9b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:23:22 +0900 Subject: [PATCH 020/160] Fixed Program --- docs/src/main/scala/00-Setup.scala | 10 +++++++--- docs/src/main/scala/01-Program.scala | 2 +- docs/src/main/scala/02-Program.scala | 2 +- docs/src/main/scala/03-Program.scala | 2 +- docs/src/main/scala/04-Program.scala | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index 8f0b2c791..4971c55b1 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -17,23 +17,25 @@ import ldbc.dsl.logging.LogHandler given LogHandler[IO] = LogHandler.noop[IO] // #given + // #setup val createDatabase: Executor[IO, Int] = sql"CREATE DATABASE IF NOT EXISTS todo".update val createTable: Executor[IO, Int] = sql""" - CREATE TABLE `task` ( + CREATE TABLE IF NOT EXISTS `task` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, `done` BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY (`id`) ) """.update + // #setup // #connection def connection = Connection[IO]( host = "127.0.0.1", - port = 3306, + port = 13306, user = "ldbc", password = Some("password") ) @@ -41,6 +43,8 @@ import ldbc.dsl.logging.LogHandler // #run connection.use { conn => - (createDatabase *> conn.setSchema("todo") *> createTable).transaction(conn) + createDatabase.commit(conn) *> + conn.setSchema("todo") *> + createTable.commit(conn) }.unsafeRunSync() // #run diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala index 62d5c03c6..6dd4ff590 100644 --- a/docs/src/main/scala/01-Program.scala +++ b/docs/src/main/scala/01-Program.scala @@ -23,7 +23,7 @@ import ldbc.dsl.logging.LogHandler // #connection def connection = Connection[IO]( host = "127.0.0.1", - port = 3306, + port = 13306, user = "ldbc", password = Some("password") ) diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala index 5a8d3682b..826d24dfe 100644 --- a/docs/src/main/scala/02-Program.scala +++ b/docs/src/main/scala/02-Program.scala @@ -24,7 +24,7 @@ import ldbc.dsl.logging.LogHandler // #connection def connection = Connection[IO]( host = "127.0.0.1", - port = 3306, + port = 13306, user = "ldbc", password = Some("password") ) diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala index d7f28d8c1..e0970bf05 100644 --- a/docs/src/main/scala/03-Program.scala +++ b/docs/src/main/scala/03-Program.scala @@ -29,7 +29,7 @@ import ldbc.dsl.logging.LogHandler // #connection def connection = Connection[IO]( host = "127.0.0.1", - port = 3306, + port = 13306, user = "ldbc", password = Some("password") ) diff --git a/docs/src/main/scala/04-Program.scala b/docs/src/main/scala/04-Program.scala index d651d4775..9fd9171ef 100644 --- a/docs/src/main/scala/04-Program.scala +++ b/docs/src/main/scala/04-Program.scala @@ -25,7 +25,7 @@ import ldbc.dsl.logging.LogHandler // #connection def connection = Connection[IO]( host = "127.0.0.1", - port = 3306, + port = 13306, user = "ldbc", password = Some("password") ) From 92857492ce8e89de8a16adcf5543004358ef9ea0 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 5 Jul 2024 22:24:26 +0900 Subject: [PATCH 021/160] Fixed docs --- README.md | 2 ++ docs/src/main/mdoc/en/index.md | 2 +- docs/src/main/mdoc/index.md | 2 ++ docs/src/main/mdoc/ja/index.md | 13 ++++++++++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d59baaa1a..4500d2d26 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effect 3 and Scala 3. +ldbc is a [Typelevel](http://typelevel.org/) project. This means we embrace pure, typeful, functional programming, and provide a safe and friendly environment for teaching, learning, and contributing as described in the Scala [Code of Conduct](http://scala-lang.org/conduct.html). + ldbc is Created under the influence of [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. Using tapir, you can build type-safe endpoints and also generate OpenAPI documentation from the endpoints you build. ldbc allows the same type-safe construction with Scala at the database layer and document generation using the constructed one. diff --git a/docs/src/main/mdoc/en/index.md b/docs/src/main/mdoc/en/index.md index b725ba5b1..f9864a76d 100644 --- a/docs/src/main/mdoc/en/index.md +++ b/docs/src/main/mdoc/en/index.md @@ -61,7 +61,7 @@ Mapping models in LDBC to table definitions is very easy. The mapping between the properties a model has and the data types defined for its columns is also very simple. The developer simply defines the corresponding columns in the same order as the properties the model has. ```scala mdoc:silent -import ldbc.core.* +import ldbc.schema.* case class User( id: Long, diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md index 85ce7d180..3aa8f2d2d 100644 --- a/docs/src/main/mdoc/index.md +++ b/docs/src/main/mdoc/index.md @@ -16,6 +16,8 @@ ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effect 3 and Scala 3. +ldbc is a [Typelevel](http://typelevel.org/) project. This means we embrace pure, typeful, functional programming, and provide a safe and friendly environment for teaching, learning, and contributing as described in the Scala [Code of Conduct](http://scala-lang.org/conduct.html). + ldbc is Created under the influence of [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. Using tapir, you can build type-safe endpoints and also generate OpenAPI documentation from the endpoints you build. ldbc allows the same type-safe construction with Scala at the database layer and document generation using the constructed one. diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index 7a3273441..d0b9900d4 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -1,4 +1,15 @@ @@@ index + * [Migration Notes](./01-Migration-Notes.md) + * [Connection](./02-Connection.md) + * [Connecting to a Database](./03-Connecting-to-a-Database.md) + * [Parameterized Queries](./04-Parameterized-Queries.md) + * [Selecting Data](./05-Selecting-Data.md) + * [Updating Data](./06-Updating-Data.md) + * [Error Handling](./07-Error-Handling.md) + * [Logging](./08-Logging.md) + * [Custom Data Type](./09-Custom-Data-Type.md) + * [Query Builder](./10-Query-Builder.md) + * [Schema](./11-Schema.md) * [Table Definitions](./01-Table-Definitions.md) * [Custom Data Type](./02-Custom-Data-Type.md) * [Type-safe Query Builder](./03-Type-safe-Query-Builder.md) @@ -7,7 +18,7 @@ * [Generating SchemaSPY Documentation](./06-Generating-SchemaSPY-Documentation.md) * [Schema Code Generation](./07-Schema-Code-Generation.md) * [Performance](./08-Perdormance.md) - * [Connector](./09-Connector.md) + * [Connector OLD](./09-Connector.md) @@@ # ldbc (Lepus Database Connectivity) From d284c024b64491f8ed67343193446cd1f4eb266c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:08:49 +0900 Subject: [PATCH 022/160] Update 07-Error-Handling --- docs/src/main/mdoc/ja/07-Error-Handling.md | 48 ++++++++-------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/docs/src/main/mdoc/ja/07-Error-Handling.md b/docs/src/main/mdoc/ja/07-Error-Handling.md index b7b4aced4..66fe76634 100644 --- a/docs/src/main/mdoc/ja/07-Error-Handling.md +++ b/docs/src/main/mdoc/ja/07-Error-Handling.md @@ -2,36 +2,6 @@ この章では、例外をトラップしたり処理したりするプログラムを構築するためのコンビネーター一式を検討する。 -@@@ vars -```yaml -version: '3' -services: - mysql: - image: mysql:"$mysqlVersion$" - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 -``` -@@@ - -次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 - -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala -``` - ## 例外について ある操作が成功するかどうかは、ネットワークの健全性、テーブルの現在の内容、ロックの状態など、予測できない要因に依存します。そのため、EitherT[Executor, Throwable, A]のような論理和ですべてを計算するか、明示的に捕捉されるまで例外の伝播を許可するかを決めなければならない。つまり、ldbcのアクション(ターゲット・モナドに変換される)が実行されると、例外が発生する可能性がある。 @@ -42,3 +12,21 @@ scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-S 2. データベース例外は、通常、ベンダー固有のSQLStateで特定のエラーを識別する一般的なSQLExceptionとして、キー違反のような一般的な状況で発生します。エラーコードは伝承として伝えられるか、実験によって発見されなければなりません。XOPENとSQL:2003の標準がありますが、どのベンダーもこれらの仕様に忠実ではないようです。これらのエラーには回復可能なものとそうでないものがある。 3. ldbcは、無効な型マッピング、ドライバから返される未知の JDBC 定数、観測される NULL 値、その他 ldbc が想定している不変条件の違反に対して InvariantViolation を発生させます。これらの例外はプログラマのエラーかドライバの不適合を示し、一般に回復不可能です。 +## モナド・エラーと派生コンバイネーター + +すべてのldbcモナドは、MonadError[?[_], Throwable]を拡張したAsyncインスタンスを提供する。つまり、Executorなどは以下のようなプリミティブな操作を持つことになる + +- raiseError: 例外を発生させる (ThrowableをM[A]に変換する) +- handleErrorWith: 例外を処理する (M[A]をM[B]に変換する) +- attempt: 例外を捕捉する (M[A]をM[Either[Throwable, A]]に変換する) + +つまり、どんなldbcプログラムでも`attempt`を加えるだけで例外を捕捉することができるのだ。 + +```scala +val program = Executor.pure[IO, Int](1) + +program.attempt +// Executor[IO, Either[Throwable, Int]] +``` + +`attempt`と`raiseError`コンビネータから、Catsのドキュメントで説明されているように、他の多くの操作を派生させることができます。 From d0ed01dfe63068ec5121c26eb2148f5d651599bb Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:11:47 +0900 Subject: [PATCH 023/160] Delete unused --- docs/src/main/mdoc/ja/01-Table-Definitions.md | 406 --------------- .../mdoc/ja/03-Type-safe-Query-Builder.md | 486 ------------------ .../main/mdoc/ja/04-Database-Connection.md | 410 --------------- docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md | 34 -- 4 files changed, 1336 deletions(-) delete mode 100644 docs/src/main/mdoc/ja/01-Table-Definitions.md delete mode 100644 docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md delete mode 100644 docs/src/main/mdoc/ja/04-Database-Connection.md delete mode 100644 docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md diff --git a/docs/src/main/mdoc/ja/01-Table-Definitions.md b/docs/src/main/mdoc/ja/01-Table-Definitions.md deleted file mode 100644 index 5b8b62612..000000000 --- a/docs/src/main/mdoc/ja/01-Table-Definitions.md +++ /dev/null @@ -1,406 +0,0 @@ -# テーブル定義 - -この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、[code generator](/ldbc/ja/07-Schema-Code-Generation.html) を使ってこの作業を省略することもできます。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import ldbc.core.* -import ldbc.core.attribute.* -``` - -LDBCは、Scalaモデルとデータベースのテーブル定義を1対1のマッピングで管理します。モデルが保持するプロパティとテーブルが保持するカラムのマッピングは、定義順に行われます。テーブル定義は、Create文の構造と非常によく似ています。このため、テーブル定義の構築はユーザーにとって直感的なものとなります。 - -LDBC は、このテーブル定義をさまざまな目的で使用します。型安全なクエリの生成、ドキュメントの生成など。 - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) // ); -``` - -すべてのカラムはcolumnメソッドで定義されます。各カラムにはカラム名、データ型、属性があります。以下のプリミティブ型が標準でサポートされており、すぐに使用できます。 - -- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` -- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` -- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` -- String -- Boolean -- java.time.* - -Null可能な列はOption[T]で表現され、Tはサポートされるプリミティブ型の1つです。Option型でない列はすべてNot Nullであることに注意してください。 - -## データ型 - -モデルが持つプロパティのScala型とカラムが持つデータ型の対応付けは、定義されたデータ型がScala型をサポートしている必要があります。サポートされていない型を割り当てようとするとコンパイルエラーが発生します。 - -データ型がサポートするScalaの型は以下の表の通りです。 - -| Data Type | Scala Type | -|------------|-----------------------------------------------------------------------------------------------| -| BIT | Byte, Short, Int, Long | -| TINYINT | Byte, Short | -| SMALLINT | Short, Int | -| MEDIUMINT | Int | -| INT | Int, Long | -| BIGINT | Long, BigInt | -| DECIMAL | BigDecimal | -| FLOAT | Float | -| DOUBLE | Double | -| CHAR | String | -| VARCHAR | String | -| BINARY | Array[Byte] | -| VARBINARY | Array[Byte] | -| TINYBLOB | Array[Byte] | -| BLOB | Array[Byte] | -| MEDIUMBLOB | Array[Byte] | -| LONGBLOB | Array[Byte] | -| TINYTEXT | String | -| TEXT | String | -| MEDIUMTEXT | String | -| DATE | java.time.LocalDate | -| DATETIME | java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime | -| TIMESTAMP | java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime | -| TIME | java.time.LocalTime | -| YEAR | java.time.Instant, java.time.LocalDate, java.time.Year | -| BOOLEAN | Boolean | - -**整数型を扱う際の注意点** - -符号あり、符号なしに応じて、扱えるデータの範囲がScalaの型に収まらないことに注意する必要があります。 - -| Data Type | signed range | unsigned range | Scala Type | range | -|-----------|--------------------------------------------|--------------------------|----------------|--------------------------------------------------------------------| -| TINYINT | -128 ~ 127 | 0 ~ 255 | Byte
Short | -128 ~ 127
-32768~32767 | -| SMALLINT | -32768 ~ 32767 | 0 ~ 65535 | Short
Int | -32768~32767
-2147483648~2147483647 | -| MEDIUMINT | -8388608 ~ 8388607 | 0 ~ 16777215 | Int | -2147483648~2147483647 | -| INT | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | Int
Long | -2147483648~2147483647
-9223372036854775808~9223372036854775807 | -| BIGINT | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 18446744073709551615 | Long
BigInt | -9223372036854775808~9223372036854775807
... | - -ユーザー定義の独自型やサポートされていない型を扱う場合は、[カスタム型](/ldbc/ja/02-Custom-Data-Type.html) を参照してください。 - -## 属性 - -カラムにはさまざまな属性を割り当てることができます。 - -- `AUTO_INCREMENT` - DDL文を作成し、SchemaSPYを文書化する際に、列を自動インクリメント・キーとしてマークする。 - MySQLでは、データ挿入時にAutoIncでないカラムを返すことはできません。そのため、必要に応じて、LDBCは戻りカラムがAutoIncとして適切にマークされているかどうかを確認します。 -- `PRIMARY_KEY` - DDL文やSchemaSPYドキュメントを作成する際に、列を主キーとしてマークする。 -- `UNIQUE_KEY` - DDL文やSchemaSPYドキュメントを作成する際に、列を一意キーとしてマークする。 -- `COMMENT` - DDL文やSchemaSPY文書を作成する際に、列にコメントを設定する。 - -## キーの設定 - -MySQLではテーブルに対してUniqueキーやIndexキー、外部キーなどの様々なキーを設定することができます。LDBCで構築したテーブル定義でこれらのキーを設定する方法を見ていきましょう。 - -### PRIMARY KEY - -主キー(primary key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムにプライマリーキー制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。また NULL も格納することができません。その結果、プライマリーキー制約が設定されたカラムの値を検索することで、テーブルの中でただ一つのデータを特定することができます。 - -LDBCではこのプライマリーキー制約を2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`PRIMARY_KEY`を渡すだけです。これによって以下の場合 `id`カラムを主キーとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) -``` - -**tableのkeySetメソッドで設定する** - -LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`PRIMARY_KEY`に主キーとして設定したいカラムを渡すことで主キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => PRIMARY_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`) -// ) -``` - -`PRIMARY_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### 複合キー (primary key) - -1つのカラムだけではなく、複数のカラムを主キーとして組み合わせ主キーとして設定することもできます。`PRIMARY_KEY`に主キーとして設定したいカラムを複数渡すだけで複合主キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => PRIMARY_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`PRIMARY_KEY`でしか設定することはできません。仮に以下のようにcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを主キーとして設定されてしまいます。 - -LDBCではテーブル定義に複数`PRIMARY_KEY`を設定したとしてもコンパイルエラーにすることはできません。しかし、テーブル定義をクエリの生成やドキュメントの生成などで使用する場合エラーとなります。これはPRIMARY KEYはテーブルごとに1つしか設定することができないという制約によるものです。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255), PRIMARY_KEY), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - -// CREATE TABLE `user` ( -// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, -// ) -``` - -### UNIQUE KEY - -一意キー(unique key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムに一意性制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。 - -LDBCではこの一意性制約を2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`UNIQUE_KEY`を渡すだけです。これによって以下の場合 `id`カラムを一意キーとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) -``` - -**tableのkeySetメソッドで設定する** - -LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`UNIQUE_KEY`に一意キーとして設定したいカラムを渡すことで一意キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => UNIQUE_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`) -// ) -``` - -`UNIQUE_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### 複合キー (unique key) - -1つのカラムだけではなく、複数のカラムを一意キーとして組み合わせ一意キーとして設定することもできます。`UNIQUE_KEY`に一意キーとして設定したいカラムを複数渡すだけで複合一意キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => UNIQUE_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`UNIQUE_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを一意キーとして設定されてしまいます。 - -### INDEX KEY - -インデックスキー(index key)とはMySQLにおいて目的のレコードを効率よく取得するための「索引」のことです。 - -LDBCではこのインデックスを2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`INDEX_KEY`を渡すだけです。これによって以下の場合 `id`カラムをインデックスとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) -``` - -**tableのkeySetメソッドで設定する** - -LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`INDEX_KEY`にインデックスとして設定したいカラムを渡すことでインデックスキーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => INDEX_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`) -// ) -``` - -`INDEX_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### 複合キー (index key) - -1つのカラムだけではなく、複数のカラムをインデックスキーとして組み合わせインデックスキーとして設定することもできます。`INDEX_KEY`にインデックスキーとして設定したいカラムを複数渡すだけで複合インデックスとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => INDEX_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`INDEX_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合インデックスとしてではなく、それぞれをインデックスキーとして設定されてしまいます。 - -### FOREIGN KEY - -外部キー(foreign key)とは、MySQLにおいてデータの整合性を保つための制約(参照整合性制約)です。 外部キーに設定されているカラムには、参照先となるテーブルのカラム内に存在している値しか設定できません。 - -LDBCではこの外部キー制約をtableのkeySetメソッドを使用する方法で設定することができます。 - -```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)) -) - -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) -// ) -``` - -`FOREIGN_KEY`メソッドにはカラムとReference値意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String - -外部キー制約には親テーブルの削除時と更新時の挙動を設定することができます。`REFERENCE`メソッドに`onDelete`と`onUpdate`メソッドが提供されているのでこちらを使用することでそれぞれ設定することができます。 - -設定することのできる値は`ldbc.core.Reference.ReferenceOption`から取得することができます。 - -```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id).onDelete(Reference.ReferenceOption.RESTRICT))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT -// ) -``` - -設定することのできる値は以下になります。 - -- `RESTRICT`: 親テーブルに対する削除または更新操作を拒否します。 -- `CASCADE`: 親テーブルから行を削除または更新し、子テーブル内の一致する行を自動的に削除または更新します。 -- `SET_NULL`: 親テーブルから行を削除または更新し、子テーブルの外部キーカラムを NULL に設定します。 -- `NO_ACTION`: 標準 SQL のキーワード。 MySQLでは、RESTRICT と同等です。 -- `SET_DEFAULT`: このアクションは MySQL パーサーによって認識されますが、InnoDB と NDB はどちらも、ON DELETE SET DEFAULT または ON UPDATE SET DEFAULT 句を含むテーブル定義を拒否します。 - -#### 複合キー (foreign key) - -1つのカラムだけではなく、複数のカラムを外部キーとして組み合わせて設定することもできます。`FOREIGN_KEY`に外部キーとして設定したいカラムを複数渡すだけで複合外部キーとして設定することができます。 - -```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("category", SMALLINT[Short]) -) - -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]), - column("post_category", SMALLINT[Short]) -) - .keySet(table => FOREIGN_KEY((table.postId, table.postCategory), REFERENCE(post, (post.id, post.category)))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`, `post_category`) REFERENCES `post` (`id`, `category`) -// ) -``` - -### 制約名 - -MySQLではCONSTRAINTを使用することで制約に対して任意の名前を付与することができます。この制約名はデータベース単位で一意の値である必要があります。 - -LDBCではCONSTRAINTメソッドが提供されているのでキー制約などの制約を設定する処理をCONSTRAINTメソッドに渡すだけで設定することができます。 - -```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => CONSTRAINT("fk_post_id", FOREIGN_KEY(table.postId, REFERENCE(post, post.id)))) - -// CREATE TABLE `user` ( -// ..., -// CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) -// ) -``` diff --git a/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md b/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md deleted file mode 100644 index 1f1c4c73c..000000000 --- a/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md +++ /dev/null @@ -1,486 +0,0 @@ -# 型安全なクエリ構築 - -この章では、LDBCで構築したテーブル定義を使用して、型安全にクエリを構築するための方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-query-builder" % "$version$" -``` -@@@ - -LDBCでのテーブル定義方法をまだ読んでいない場合は、[テーブル定義](/ldbc/ja/01-Table-Definitions.html)の章を先に読むことをオススメしましす。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import cats.effect.IO -import ldbc.core.* -import ldbc.query.builder.TableQuery -``` - -LDBCではTableQueryにテーブル定義を渡すことで型安全なクエリ構築を行います。 - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) - -val userQuery = TableQuery[User](table) -``` - -## SELECT - -型安全にSELECT文を構築する方法はTableQueryが提供する`select`メソッドを使用することです。LDBCではプレーンなクエリに似せて実装されているため直感的にクエリ構築が行えます。またどのようなクエリが構築されているかも一目でわかるような作りになっています。 - -特定のカラムのみ取得を行うSELECT文を構築するには`select`メソッドで取得したいカラムを指定するだけです。 - -```scala 3 -val select = userQuery.select(_.id) - -select.statement === "SELECT `id` FROM user" -``` - -複数のカラムを指定する場合は`select`メソッドで取得したいカラムを指定して指定したカラムのタプルを返すだけです。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name)) - -select.statement === "SELECT `id`, `name` FROM user" -``` - -全てのカラムを指定したい場合はTableQueryが提供する`selectAll`メソッドを使用することで構築できます。 - -```scala 3 -val select = userQuery.selectAll - -select.statement === "SELECT `id`, `name`, `age` FROM user" -``` - -特定のカラムの数を取得したい場合は、指定したカラムで`count`を使用することで構築できます。  - -```scala 3 -val select = userQuery.select(_.id.count) - -select.statement === "SELECT COUNT(id) FROM user" -``` - -### WHERE - -クエリに型安全にWhere条件を設定する方法は`where`メソッドを使用することです。 - -```scala 3 -val select = userQuery.select(_.id).where(_.name === "Test") - -select.statement === "SELECT `id` FROM user WHERE name = ?" -``` - -`where`メソッドで使用できる条件の一覧は以下です。 - -| 条件 | ステートメント | -|--------------------------------------|---------------------------------------| -| === | `column = ?` | -| >= | `column >= ?` | -| > | `column > ?` | -| <= | `column <= ?` | -| < | `column < ?` | -| <> | `column <> ?` | -| !== | `column != ?` | -| IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL") | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | -| <=> | `column <=> ?` | -| IN (value, value, ...) | `column IN (?, ?, ...)` | -| BETWEEN (start, end) | `column BETWEEN ? AND ?` | -| LIKE (value) | `column LIKE ?` | -| LIKE_ESCAPE (like, escape) | `column LIKE ? ESCAPE ?` | -| REGEXP (value) | `column REGEXP ?` | -| `<<` (value) | `column << ?` | -| `>>` (value) | `column >> ?` | -| DIV (cond, result) | `column DIV ? = ?` | -| MOD (cond, result) | `column MOD ? = ?` | -| ^ (value) | `column ^ ?` | -| ~ (value) | `~column = ?` | - -### GROUP BY/Having - -クエリに型安全にGROUP BY句を設定する方法は`groupBy`メソッドを使用することです。 - -`groupBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準にグループ化することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3) - -select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age" -``` - -グループ化すると`select`で取得できるデータの数はグループの数だけとなります。そこでグループ化を行った場合には、グループ化に指定したカラムの値や、用意された関数を使ってカラムの値をグループ単位で集計した結果などを取得することができます。 - -`having`を使用すると`groupBy`によってグループ化されて取得したデータに関して、取得する条件を設定することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3).having(_._3 > 20) - -select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age HAVING age > ?" -``` - -### ORDER BY - -クエリに型安全にORDER BY句を設定する方法は`orderBy`メソッドを使用することです。 - -`orderBy`を使うことで`select`でデータを取得する時に指定したカラムの値を対象にソートした結果を取得することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age) - -select.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age" -``` - -昇順/降順を指定したい場合は、それぞれカラムに対して `asc`/`desc`を呼び出すだけです。 - -```scala 3 -val desc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.desc) - -desc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age DESC" - -val asc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.asc) - -asc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age ASC" -``` - -### LIMIT/OFFSET - -クエリに型安全にLIMIT句とOFFSET句を設定する方法は`limit`/`offset`メソッドを使用することです。 - -`limit`を設定すると`select`を実行した時に取得するデータの行数の上限を設定することができ、`offset`を設定すると何番目からのデータを取得するのかを指定することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).limit(100).offset(50) - -select.statement === "SELECT `id`, `name`, `age` FROM user LIMIT ? OFFSET ?" -``` - -## JOIN/LEFT JOIN/RIGHT JOIN - -クエリに型安全にJoinを設定する方法は`join`/`leftJoin`/`rightJoin`メソッドを使用することです。 - -Joinでは以下定義をサンプルとして使用します。 - -```scala 3 -case class Country(code: String, name: String) -object Country: - val table = Table[Country]("country")( - column("code", CHAR(3), PRIMARY_KEY), - column("name", VARCHAR(255)) - ) - -case class City(id: Long, name: String, countryCode: String) -object City: - val table = Table[City]("city")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("country_code", CHAR(3)) - ) - -case class CountryLanguage( - countryCode: String, - language: String -) -object CountryLanguage: - val table: Table[CountryLanguage] = Table[CountryLanguage]("country_language")( - column("country_code", CHAR(3)), - column("language", CHAR(30)) - ) - -val countryQuery = TableQuery[Country](Country.table) -val cityQuery = TableQuery[City](City.table) -val countryLanguageQuery = TableQuery[CountryLanguage](CountryLanguage.table) -``` - -まずシンプルなJoinを行いたい場合は、`join`を使用します。 -`join`の第一引数には結合したいテーブルを渡し、第二引数では結合元のテーブルと結合したいテーブルのカラムで比較を行う関数を渡します。これはJoinにおいてのON句に該当します。 - -Join後の`select`は2つのテーブルからカラムを指定することになります。 - -```scala 3 -val join = countryQuery.join(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country JOIN city ON country.code = city.country_code" -``` - -次に左外部結合であるLeft Joinを行いたい場合は、`leftJoin`を使用します。 -`join`が`leftJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 - -```scala 3 -val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city ON country.code = city.country_code" -``` - -シンプルなJoinとの違いは`leftJoin`を使用した場合、結合を行うテーブルから取得するレコードはNULLになる可能性があるということです。 - -そのためLDBCでは`leftJoin`に渡されたテーブルから取得するカラムのレコードは全てOption型になります。 - -```scala 3 -val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (String, Option[String]) -``` - -次に右外部結合であるRight Joinを行いたい場合は、`rightJoin`を使用します。 -こちらも`join`が`rightJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 - -```scala 3 -val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN city ON country.code = city.country_code" -``` - -シンプルなJoinとの違いは`rightJoin`を使用した場合、結合元のテーブルから取得するレコードはNULLになる可能性があるということです。 - -そのためLDBCでは`rightJoin`を使用した結合元のテーブルから取得するカラムのレコードは全てOption型になります。 - -```scala 3 -val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (Option[String], String) -``` - -複数のJoinを行いたい場合は、メソッドチェーンで任意のJoinメソッドを呼ぶことで実現することができます。 - -```scala 3 -val join = - (countryQuery join cityQuery)((country, city) => country.code === city.countryCode) - .rightJoin(countryLanguageQuery)((_, city, countryLanguage) => city.countryCode === countryLanguage.countryCode) - .select((country, city, countryLanguage) => (country.name, city.name, countryLanguage.language)) // (Option[String], Option[String], String)] - -join.statement = - """ - |SELECT - | country.`name`, - | city.`name`, - | country_language.`language` - |FROM country - |JOIN city ON country.code = city.country_code - |RIGHT JOIN country_language ON city.country_code = country_language.country_code - |""".stripMargin -``` - -複数のJoinを行っている状態で`rightJoin`での結合を行うと、今までの結合が何であったかにかかわらず直前まで結合していたテーブルから取得するレコードは全てNULL許容なアクセスとなることに注意してください。 - -## Custom Data Type - -前章でユーザー独自の型もしくはサポートされていない型を使用するためにDataTypeの`mapping`メソッドを使用して独自の型とDataTypeのマッピングを行ないました。([参照](/ldbc/ja/02-Custom-Data-Type.html)) - -LDBCはテーブル定義とデータベースへの接続処理が分離されています。 -そのためデータベースからデータを取得する際にユーザー独自の型もしくはサポートされていない型に変換したい場合は、ResultSetからのデータ取得方法を独自の型もしくはサポートされていない型と紐付けてあげる必要があります。 - -例えばユーザー定義のEnumを文字列型とマッピングしたい場合は、以下のようになります。 - -```scala 3 -enum Custom: - case ... - -given ResultSetReader[IO, Custom] = - ResultSetReader.mapping[IO, str, Custom](str => Custom.valueOf(str)) -``` - -※ この処理は将来のバージョンでDataTypeのマッピングと統合される可能性があります。 - -## INSERT - -型安全にINSERT文を構築する方法はTableQueryが提供する以下のメソッドを使用することです。 - -- insert -- insertInto -- += -- ++= - -**insert** - -`insert`メソッドには挿入するデータのタプルを渡します。タプルはモデルと同じプロパティの数と型である必要があります。また、挿入されるデータの順番はモデルのプロパティおよびテーブルのカラムと同じ順番である必要があります。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" -``` - -複数のデータを挿入したい場合は、`insert`メソッドに複数のタプルを渡すことで構築できます。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None), (2L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -**insertInto** - -`insert`メソッドはテーブルが持つ全てのカラムにデータ挿入を行いますが、特定のカラムに対してのみデータを挿入したい場合は`insertInto`メソッドを使用します。 - -これはAutoIncrementやDefault値を持つカラムへのデータ挿入を除外したい場合などに使用できます。 - -```scala 3 -val insert = userQuery.insertInto(user => (user.name, user.age)).values(("name", None)) - -insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?)" -``` - -複数のデータを挿入したい場合は、`values`にタプルの配列を渡すことで構築できます。 - -```scala 3 -val insert = userQuery.insertInto(user => (user.name, user.age)).values(List(("name", None), ("name", Some(20)))) - -insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?), (?, ?)" -``` - -**+=** - -`+=`メソッドを使用することでモデルを使用してinsert文を構築することができます。モデルを使用する場合は全てのカラムにデータを挿入してしまうことに注意してください。 - -```scala 3 -val insert = userQuery += User(1L, "name", None) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" -``` - -**++=** - -モデルを使用して複数のデータを挿入したい場合は`++=`メソッドを使用します。 - -```scala 3 -val insert = userQuery ++= List(User(1L, "name", None), User(2L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -### ON DUPLICATE KEY UPDATE - -ON DUPLICATE KEY UPDATE 句を指定し行を挿入すると、UNIQUEインデックスまたはPRIMARY KEYで値が重複する場合、古い行のUPDATEが発生します。 - -LDBCでこの処理を実現する方法は2種類あり、`insertOrUpdate{s}`を使用するか、`Insert`に対して`onDuplicateKeyUpdate`を使用することです。 - -```scala 3 -val insert = userQuery.insertOrUpdate((1L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `id` = new_user.`id`, `name` = new_user.`name`, `age` = new_user.`age`" -``` - -`insertOrUpdate{s}`を使用した場合、全てのカラムが更新対象となることに注意してください。重複する値があり特定のカラムのみを更新したい場合は、`onDuplicateKeyUpdate`を使用して更新したいカラムのみを指定するようにしてください。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).onDuplicateKeyUpdate(v => (v.name, v.age)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `age` = new_user.`age`" -``` - -## UPDATE - -型安全にUPDATE文を構築する方法はTableQueryが提供する`update`メソッドを使用することです。 - -`update`メソッドの第1引数にはテーブルのカラム名ではなくモデルのプロパティ名を指定し、第2引数に更新したい値を渡します。第2引数に渡す値の型は第1引数で指定したプロパティの型と同じである必要があります。 - -```scala 3 -val update = userQuery.update("name", "update name") - -update.statement === "UPDATE user SET name = ?" -``` - -第1引数に存在しないプロパティ名を指定した場合コンパイルエラーとなります。 - -```scala 3 -val update = userQuery.update("hoge", "update name") // Compile error -``` - -複数のカラムを更新したい場合は`set`メソッドを使用します。 - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20)) - -update.statement === "UPDATE user SET name = ?, age = ?" -``` - -`set`メソッドには条件に応じてクエリを生成させないようにすることもできます。 - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20), false) - -update.statement === "UPDATE user SET name = ?" -``` - -モデルを使用してupdate文を構築することもできます。モデルを使用する場合は全てのカラムを更新してしまうことに注意してください。 - -```scala 3 -val update = userQuery.update(User(1L, "update name", None)) - -update.statement === "UPDATE user SET id = ?, name = ?, age = ?" -``` - -### WHERE - -`where`メソッドを使用することでupdate文にもWhere条件を設定することができます。 - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20)).where(_.id === 1) - -update.statement === "UPDATE user SET name = ?, age = ? WHERE id = ?" -``` - -`where`メソッドで使用できる条件はInsert文の[where項目](/ldbc/ja/03-Type-safe-Query-Builder.html#where)を参照してください。 - -## DELETE - -型安全にDELETE文を構築する方法はTableQueryが提供する`delete`メソッドを使用することです。 - -```scala 3 -val delete = userQuery.delete - -delete.statement === "DELETE FROM user" -``` - -### WHERE - -`where`メソッドを使用することでdelete文にもWhere条件を設定することができます。 - -```scala 3 -val delete = userQuery.delete.where(_.id === 1) - -delete.statement === "DELETE FROM user WHERE id = ?" -``` - -`where`メソッドで使用できる条件はInsert文の[where項目](/ldbc/ja/03-Type-safe-Query-Builder.html#where)を参照してください。 - -## DDL - -型安全にDDLを構築する方法はTableQueryが提供する以下のメソッドを使用することです。 - -- createTable -- dropTable -- truncateTable - -spec2を使用している場合は以下のようにしてテストの前後にDDLを実行することができます。 - -```scala 3 -import cats.effect.IO -import cats.effect.unsafe.implicits.global - -import org.specs2.mutable.Specification -import org.specs2.specification.core.Fragments -import org.specs2.specification.BeforeAfterEach - -object Test extends Specification, BeforeAfterEach: - - override def before: Fragments = - step((tableQuery.createTable.update.autoCommit(dataSource) >> IO.println("Complete create table")).unsafeRunSync()) - - override def after: Fragments = - step((tableQuery.dropTable.update.autoCommit(dataSource) >> IO.println("Complete drop table")).unsafeRunSync()) -``` diff --git a/docs/src/main/mdoc/ja/04-Database-Connection.md b/docs/src/main/mdoc/ja/04-Database-Connection.md deleted file mode 100644 index 102598b5c..000000000 --- a/docs/src/main/mdoc/ja/04-Database-Connection.md +++ /dev/null @@ -1,410 +0,0 @@ -# データベース接続 - -この章では、LDBCで構築したクエリを使用して、データベースへの接続処理を行うための方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -@@@ vars -```scala -libraryDependencies ++= Seq( - "$org$" %% "ldbc-dsl" % "$version$", - "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" -) -``` -@@@ - -LDBCでのクエリ構築方法をまだ読んでいない場合は、[型安全なクエリ構築](/ldbc/ja/03-Type-safe-Query-Builder.html)の章を先に読むことをオススメしましす。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import com.mysql.cj.jdbc.MysqlDataSource - -import cats.effect.IO -// This is just for testing. Consider using cats.effect.IOApp instead of calling -// unsafe methods directly. -import cats.effect.unsafe.implicits.global - -import ldbc.sql.* -import ldbc.dsl.io.* -import ldbc.dsl.logging.ConsoleLogHandler -import ldbc.query.builder.TableQuery -``` - -テーブル定義は以下を使用します。 - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) - -val userQuery = TableQuery[User](table) -``` - -## DataSourceの使用 - -LDBCはデータベース接続にJDBCのDataSourceを使用します。LDBCにはこのDataSourceを構築する実装は提供されていないため、mysqlやHikariCPなどのライブラリを使用する必要があります。今回の例ではMysqlDataSourceを使用してDataSourceの構築を行います。 - -```scala 3 -private val dataSource = new MysqlDataSource() -dataSource.setServerName("127.0.0.1") -dataSource.setPortNumber(3306) -dataSource.setDatabaseName("database name") -dataSource.setUser("user name") -dataSource.setPassword("password") -``` - -## ログ - -LDBCではDatabase接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 - -標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 - -```scala 3 -given LogHandler[IO] = ConsoleLogHandler[IO] -``` - -### カスタマイズ - -任意のロギングライブラリを使用してログをカスタマイズする場合は`ldbc.dsl.logging.LogHandler`を使用します。 - -以下は標準実装のログ実装です。LDBCではデータベース接続で以下3種類のイベントが発生します。 - -- Success: 処理の成功 -- ProcessingFailure: データ取得後もしくはデータベース接続前の処理のエラー -- ExecFailure: データベースへの接続処理のエラー - -それぞれのイベントでどのようなログを書き込むかをパターンマッチングによって振り分けを行います。 - -```scala 3 -def consoleLogger[F[_]: Console: Sync]: LogHandler[F] = - case LogEvent.Success(sql, args) => - Console[F].println( - s"""Successful Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) - case LogEvent.ProcessingFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed ResultSet Processing: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) - case LogEvent.ExecFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) -``` - -## Query - -`select`文を構築すると`toList`/`headOption`/`unsafe`メソッドを使用できるようになります。これらのメソッドは取得後のデータ形式を決定するために使用します。特段何も型を指定しない場合は`select`メソッドで指定したカラムの型がTupleとして返却されます。 - -### toList - -クエリを実行した結果データの一覧を取得したい場合は、`toList`メソッドを使用します。`toList`メソッドを使用してデータベース処理を行なった結果、データ取得件数が0件であった場合空の配列が返されます。 - -```scala 3 -val query1 = userQuery.selectAll.toList // List[(Long, String, Option[Int])] -``` - -`toList`メソッドにモデルを指定すると取得後のデータを指定したモデルに変換することができます。 - -```scala 3 -val query = userQuery.selectAll.toList[User] // User -``` - -`toList`メソッドで指定するモデルの型は`select`メソッドで指定したTupleの型と一致するか、Tupleの型から指定したモデルへの型変換が可能なものでなければなりません。 - -```scala 3 -val query1 = userQuery.select(user => (user.name, user.age)).toList[User] // Compile error - -case class Test(name: String, age: Option[Int]) -val query2 = userQuery.select(user => (user.name, user.age)).toList[Test] // Test -``` - -### headOption - -クエリを実行した結果最初の1件のデータをOptionalで取得したい場合は、`headOption`メソッドを使用します。`headOption`メソッドを使用してデータベース処理を行なった結果データ取得件数が0件であった場合Noneが返されます。 - -`headOption`メソッドを使用した場合、複数のデータを取得するクエリを実行したとしても最初のデータのみ返されることに注意してください。 - -```scala 3 -val query1 = userQuery.selectAll.headOption // Option[(Long, String, Option[Int])] -val query2 = userQuery.selectAll.headOption[User] // Option[User] -``` - -### unsafe - -`unsafe`メソッドを使用した場合、取得したデータの最初の1件のみ返されることは`headOption`メソッドと同じですが、データはOptionalにはならずそのままのデータが返却されます。もし取得したデータの件数が0件であった場合は例外が発生するため適切な例外ハンドリングを行う必要があります。 - -実行時に例外を発生する可能性が高いため`unsafe`という名前になっています。 - -```scala 3 -val query1 = userQuery.selectAll.unsafe // (Long, String, Option[Int]) -val query2 = userQuery.selectAll.unsafe[User] // User -``` - -## Update - -`insert/update/delete`文を構築すると`update`メソッドを使用できるようになります。`update`メソッドはデータベースへの書き込み処理件数を返却します。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).update // Int -val update = userQuery.update("name", "update name").update // Int -val delete = userQuery.delete.update // Int -``` - -`insert`文の場合データ挿入時にAutoIncrementで生成された値を返却させたい場合があります。その場合は`update`メソッドではなく`returning`メソッドを使用して返却したいカラムを指定します。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).returning("id") // Long -``` - -`returning`メソッドで指定する値はモデルが持つプロパティ名である必要があります。また、指定したプロパティがテーブル定義上でAutoIncrementの属性が設定されていなければエラーとなってしまいます。 - -MySQLではデータ挿入時に返却できる値はAutoIncrementのカラムのみであるため、LDBCでも同じような仕様となっています。 - -## データベース操作の実行 - -データベース接続を行う前にコミットのタイミングや読み書き専用などの設定を行う必要があります。 - -### 読み取り専用 - -`readOnly`メソッドを使用することで実行するクエリの処理を読み込み専用にすることができます。`readOnly`メソッドは`insert/update/delete`文でも使用することができますが、書き込み処理を行うので実行時にエラーとなります。 - -```scala 3 -val read = userQuery.selectAll.toList.readOnly(dataSource) -``` - -### 自動コミット - -`autoCommit`メソッドを使用することで実行するクエリの処理をクエリ実行時ごとにコミットするように設定することができます。 - -```scala 3 -val read = userQuery.insert((1L, "name", None)).update.autoCommit(dataSource) -``` - -### トランザクション - -`transaction`メソッドを使用することで複数のデータベース接続処理を1つのトランザクションにまとめることができます。 - -`toList/headOption/unsafe/returning/update`メソッドの戻り値は`Kleisli[F, Connection[F], T]`型となっています。そのためmapやflatMapを使用して処理を1つにまとめることができます。 - -1つにまとめた`Kleisli[F, Connection[F], T]`に対して`transaction`メソッドを使用することで、中で行われる全てのデータベース接続処理は1つのトランザクションにまとめて実行されます。 - -```scala 3 -(for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction(dataSource) -``` - -## Database Action - -データベース処理を実行する方法としてデータベースへの接続情報を持った`Database`を使用して行う方法も存在します。 - -`Database`を構築する方法はDriverManagerを使用した方法と、DataSourceから生成する方法の2種類があります。以下はMySQLのドライバーを使用してデータベースへの接続情報を持った`Database`を構築する例です。 - -```scala 3 -val db = Database.fromMySQLDriver[IO]("database name", "host", "port number", "user name", "password") -``` - -`Database`を使用してデータベース処理を実行するメリットは以下になります。 - -- DataSourceの構築を簡略できる (DriverManagerを使用した場合) -- クエリごとにDataSourceを受け渡す必要がなくなる - -`Database`を使用する方法は、DataSourceを受け渡す方法を簡略化しただけにすぎないため、どちらを使用しても実行結果に差が出ることはありません。 -`flatMap`などで処理を結合しメソッドチェーンで実行するか、結合した処理を`Database`を使用して実行するかの違いでしかありません。そのため実行方法はユーザーの好きの方法を選択できます。 - -**Read Only** - -```scala 3 -val user: Option[User] = db.readOnly(userQuery.selectAll.headOption[User]).unsafeRunSync() -``` - -**Auto Commit** - -```scala 3 -val result = db.autoCommit(userQuery.insert((1L, "name", None)).update).unsafeRunSync() -``` - -**Transaction** - -```scala 3 -db.transaction(for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).unsafeRunSync() -``` - -### Database model - -LDBCでは`Database`モデルはデータベースの接続情報を持つ以外の用途でも使用されます。他の用途としてSchemaSPYのドキュメント生成に使用されることです。SchemaSPYのドキュメント生成に関しては[こちら](/ldbc/ja/06-Generating-SchemaSPY-Documentation.html)を参照してください。 - -すでに`Database`モデルを別の用途で生成している場合は、そのモデルを使用してデータベースの接続情報を持った`Database`を構築することができます。 - -```scala 3 -import ldbc.dsl.io.* - -val database: Database = ??? - -val db = database.fromDriverManager() -// or -val db = database.fromDriverManager("user name", "password") -``` - -### メソッドチェーンでの使用 - -`Database`モデルは`TableQuery`のメソッドで`DataSource`の代わりに使用することもできます。 - -```scala 3 -val read = userQuery.selectAll.toList.readOnly(db) -val commit = userQuery.insert((1L, "name", None)).update.autoCommit(db) -val transaction = (for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction(db) -``` - -## HikariCPコネクションプールの使用 - -`ldbc-hikari`は、HikariCP接続プールを構築するためのHikariConfigおよびHikariDataSourceを構築するためのビルダーを提供します。 - -@@@ vars -```scala -libraryDependencies ++= Seq( - "$org$" %% "ldbc-hikari" % "$version$", -) -``` -@@@ - -`HikariConfigBuilder`は名前の通りHikariCPの`HikariConfig`を構築するためのビルダーです。 - -```scala 3 -val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.default.build() -``` - -`HikariConfigBuilder`には`default`と`from`メソッドがあり`default`を使用した場合、LDBC指定のパスを元にConfigから対象の値を取得して`HikariConfig`の構築を行います。 - -```text -ldbc.hikari { - jdbc_url = ... - username = ... - password = ... -} -``` - -ユーザー独自のパスを指定したい場合は`from`メソッドを使用して引数に取得したいパスを渡す必要があります。 - -```scala 3 -val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.from("custom.path").build() - -// custom.path { -// jdbc_url = ... -// username = ... -// password = ... -// } -``` - -HikariCPに設定できる内容は[公式](https://github.com/brettwooldridge/HikariCP)を参照してください。 - -Configに設定できるキーの一覧は以下になります。 - -| キー名 | 説明 | 型 | -|-----------------------------|------------------------------------------------------------------------|----------| -| catalog | 接続時に設定するデフォルトのカタログ名 | String | -| connection_timeout | クライアントがプールからの接続を待機する最大ミリ秒数 | Duration | -| idle_timeout | 接続がプール内でアイドル状態であることを許可される最大時間 (ミリ秒単位) | Duration | -| leak_detection_threshold | 接続漏れの可能性を示すメッセージがログに記録されるまでに、接続がプールから外れる時間 | Duration | -| maximum_pool_size | アイドル接続と使用中の接続の両方を含め、プールが許容する最大サイズ | Int | -| max_lifetime | プール内の接続の最大寿命 | Duration | -| minimum_idle | アイドル接続と使用中接続の両方を含め、HikariCPがプール内に維持しようとするアイドル接続の最小数 | Int | -| pool_name | 接続プールの名前 | String | -| allow_pool_suspension | プール・サスペンドを許可するかどうか | Boolean | -| auto_commit | プール内の接続のデフォルトの自動コミット動作 | Boolean | -| connection_init_sql | 新しい接続が作成されたときに、その接続がプールに追加される前に実行されるSQL文字列 | String | -| connection_test_query | 接続の有効性をテストするために実行する SQL クエリ | String | -| data_source_classname | Connections の作成に使用する JDBC DataSourceの完全修飾クラス名 | String | -| initialization_fail_timeout | プール初期化の失敗タイムアウト | Duration | -| isolate_internal_queries | 内部プール・クエリ (主に有効性チェック)を、`Connection.rollback()`によって独自のトランザクションで分離するかどうか | Boolean | -| jdbc_url | JDBCのURL | String | -| readonly | プールに追加する接続を読み取り専用接続として設定するかどうか | Boolean | -| register_mbeans | HikariCPがJMXにHikariConfigMXBeanとHikariPoolMXBeanを自己登録するかどうか | Boolean | -| schema | 接続時に設定するデフォルトのスキーマ名 | String | -| username | `DataSource.getConnection(username,password)`の呼び出しに使用されるデフォルトのユーザ名 | String | -| password | `DataSource.getConnection(username,password)`の呼び出しに使用するデフォルトのパスワード | String | -| driver_class_name | 使用するDriverのクラス名 | String | -| transaction_isolation | デフォルトのトランザクション分離レベル | String | - -`HikariDataSourceBuilder`を使用することで、HikariCPの`HikariDataSource`を構築することができます。 - -接続プールはライフタイムで管理されるオブジェクトでありきれいにシャットダウンする必要があるため、ビルダーによって構築された`HikariDataSource`は`Resource`として管理されます。 - -```scala 3 -val dataSource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() -``` - -`buildDataSource`経由で構築された`HikariDataSource`は、内部でLDBC指定のパスを元にConfigから設定を取得し構築された`HikariConfig`を使用しています。 -これは`HikariConfigBuilder`の`default`経由で生成された`HikariConfig`と同等のものです。 - -もしユーザー指定の`HikariConfig`を使用したい場合は、`buildFromConfig`を使用することで`HikariDataSource`を構築することができます。 - -```scala 3 -val hikariConfig = ??? -val dataSource = HikariDataSourceBuilder.default[IO].buildFromConfig(hikariConfig) -``` - -`HikariDataSourceBuilder`を使用して構築された`HikariDataSource`は通常IOAppを使用して実行します。 - -```scala 3 -object HikariApp extends IOApp: - - val dataSourceResource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() - - def run(args: List[String]): IO[ExitCode] = - dataSourceResource.use { dataSource => - ... - } -``` - -### HikariDatabase - -HikariCPのコネクション情報を持った`Database`を構築する方法も存在します。 - -`HikariDatabase`は`HikariDataSource`と同様に`Resource`として管理されます。 -そのため通常はIOAppを使用して実行します。 - -```scala 3 -object HikariApp extends IOApp: - - val hikariConfig = ??? - val databaseResource: Resource[F, Database[F]] = HikariDatabase.fromHikariConfig[IO](hikariConfig) - - def run(args: List[String]): IO[ExitCode] = - databaseResource.use { database => - for - result <- database.readOnly(...) - yield ExitCode.Success - } -``` diff --git a/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md b/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md deleted file mode 100644 index e52030ef7..000000000 --- a/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md +++ /dev/null @@ -1,34 +0,0 @@ -# プレーンなSQLクエリ - -時には、抽象度の高いレベルではうまくサポートされていない操作のために、独自のSQLコードを書く必要があるかもしれません。JDBCの低レイヤーに戻る代わりに、ScalaベースのAPIでLDBCのPlain SQLクエリーを使うことができます。 -この章では、そのような場合にLDBCでPlain SQLクエリーを使用してデータベースへの接続処理を行うための方法について説明します。 - -プロジェクトへの依存関係やDataSourceの使用とログに関しては、前章の[データベース接続](/ldbc/ja/04-Database-Connection.html)の章を参照してください。 - -## Plain SQL - -LDBCでは以下のようにsql文字列補間をリテラルSQL文字列で使用してプレーンなクエリを構築します。 - -クエリに注入された変数や式は、結果のクエリ文字列のバインド変数に変換されます。クエリ文字列に直接挿入されるわけではないので、SQLインジェクション攻撃の危険はありません。 - -```scala 3 -val select = sql"SELECT id, name, age FROM user WHERE id = $id" // SELECT id, name, age FROM user WHERE id = ? -val insert = sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)" // INSERT INTO user (id, name, age) VALUES(?, ?, ?) -val update = sql"UPDATE user SET id = $id, name = $name, age = $age" // UPDATE user SET id = ?, name = ?, age = ? -val delete = sql"DELETE FROM user WHERE id = $id" // DELETE FROM user WHERE id = ? -``` - -Plain SQLクエリーは実行時にSQL文を構築するだけです。これは安全かつ簡単に複雑なステートメントを構築する方法を提供しますが、これは単なる埋め込み文字列にすぎません。ステートメントに構文エラーがあったり、データベースとScalaコードの型が一致しなかったりしてもコンパイル時に検出することはできません。 - -クエリ実行結果の戻り値の型、接続方法の設定に関しては前章の「データベース接続」にある[Query](/ldbc/ja/04-Database-Connection.html#Query)項目以降を参照してください。 -テーブル定義を使用して構築されたクエリと同じように構築および動作します。 - -プレーンなクエリと型安全なクエリは構築方法が違うだけで後続の接続方法などは同じ実装です。そのため2つを組み合わせてクエリを実行することも可能です。 - -```scala 3 -(for - result1 <- sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)".update - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction -``` From 1696b46c8020c9509459e69083726e014d2c6bc4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:13:28 +0900 Subject: [PATCH 024/160] Rename old document --- ...n.md => 12-Generating-SchemaSPY-Documentation.md} | 0 ...de-Generation.md => 13-Schema-Code-Generation.md} | 0 .../mdoc/ja/{08-Perdormance.md => 14-Perdormance.md} | 0 .../mdoc/ja/{09-Connector.md => 15-Connector.md} | 0 docs/src/main/mdoc/ja/index.md | 12 ++++-------- 5 files changed, 4 insertions(+), 8 deletions(-) rename docs/src/main/mdoc/ja/{06-Generating-SchemaSPY-Documentation.md => 12-Generating-SchemaSPY-Documentation.md} (100%) rename docs/src/main/mdoc/ja/{07-Schema-Code-Generation.md => 13-Schema-Code-Generation.md} (100%) rename docs/src/main/mdoc/ja/{08-Perdormance.md => 14-Perdormance.md} (100%) rename docs/src/main/mdoc/ja/{09-Connector.md => 15-Connector.md} (100%) diff --git a/docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md b/docs/src/main/mdoc/ja/12-Generating-SchemaSPY-Documentation.md similarity index 100% rename from docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md rename to docs/src/main/mdoc/ja/12-Generating-SchemaSPY-Documentation.md diff --git a/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md b/docs/src/main/mdoc/ja/13-Schema-Code-Generation.md similarity index 100% rename from docs/src/main/mdoc/ja/07-Schema-Code-Generation.md rename to docs/src/main/mdoc/ja/13-Schema-Code-Generation.md diff --git a/docs/src/main/mdoc/ja/08-Perdormance.md b/docs/src/main/mdoc/ja/14-Perdormance.md similarity index 100% rename from docs/src/main/mdoc/ja/08-Perdormance.md rename to docs/src/main/mdoc/ja/14-Perdormance.md diff --git a/docs/src/main/mdoc/ja/09-Connector.md b/docs/src/main/mdoc/ja/15-Connector.md similarity index 100% rename from docs/src/main/mdoc/ja/09-Connector.md rename to docs/src/main/mdoc/ja/15-Connector.md diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index d0b9900d4..e1dd3a0e1 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -10,15 +10,11 @@ * [Custom Data Type](./09-Custom-Data-Type.md) * [Query Builder](./10-Query-Builder.md) * [Schema](./11-Schema.md) - * [Table Definitions](./01-Table-Definitions.md) + * [Generating SchemaSPY Documentation](./12-Generating-SchemaSPY-Documentation.md) + * [Schema Code Generation](./13-Schema-Code-Generation.md) + * [Performance](./14-Perdormance.md) + * [Connector OLD](./15-Connector.md) * [Custom Data Type](./02-Custom-Data-Type.md) - * [Type-safe Query Builder](./03-Type-safe-Query-Builder.md) - * [Database Connection](./04-Database-Connection.md) - * [Plain SQL Queries](./05-Plain-SQL-Queries.md) - * [Generating SchemaSPY Documentation](./06-Generating-SchemaSPY-Documentation.md) - * [Schema Code Generation](./07-Schema-Code-Generation.md) - * [Performance](./08-Perdormance.md) - * [Connector OLD](./09-Connector.md) @@@ # ldbc (Lepus Database Connectivity) From 31487d19f1af3c2554389f8beead5bef1ea68657 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:15:47 +0900 Subject: [PATCH 025/160] Replace custom data type document into 11-Schema --- docs/src/main/mdoc/ja/02-Custom-Data-Type.md | 78 -------------------- docs/src/main/mdoc/ja/11-Schema.md | 71 ++++++++++++++++++ 2 files changed, 71 insertions(+), 78 deletions(-) delete mode 100644 docs/src/main/mdoc/ja/02-Custom-Data-Type.md diff --git a/docs/src/main/mdoc/ja/02-Custom-Data-Type.md b/docs/src/main/mdoc/ja/02-Custom-Data-Type.md deleted file mode 100644 index 8a3c9ea06..000000000 --- a/docs/src/main/mdoc/ja/02-Custom-Data-Type.md +++ /dev/null @@ -1,78 +0,0 @@ -# カスタム データ型 - -この章では、LDBCで構築したテーブル定義でユーザー独自の型もしくはサポートされていない型を使用するための方法について説明します。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import ldbc.core.* -``` - -ユーザー独自の型もしくはサポートされていない型を使用するための方法はカラムのデータ型をどのような型として扱うかを教えてあげることです。DataTypeには`mapping`メソッドが提供されているのでこのメソッドを使用して暗黙の型変換として設定します。 - -```scala 3 -case class User( - id: Long, - name: User.Name, - age: Option[Int], -) - -object User: - - case class Name(firstName: String, lastName: String) - - given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` - -LDBCでは複数のカラムをモデルが持つ1つのプロパティに統合することはできません。LDBCの目的はモデルとテーブルを1対1でマッピングを行い、データベースのテーブル定義を型安全に構築することにあるからです。 - -そのためテーブル定義とモデルで異なった数のプロパティを持つようなことは許可していません。以下のような実装はコンパイルエラーとなります。 - -```scala 3 -case class User( - id: Long, - name: User.Name, - age: Option[Int], -) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` - -上記のような実装を行いたい場合は以下のような実装を検討してください。 - -```scala 3 -case class User( - id: Long, - firstName: String, - lastName: String, - age: Option[Int], -): - - val name: User.Name = User.Name(firstName, lastName) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` diff --git a/docs/src/main/mdoc/ja/11-Schema.md b/docs/src/main/mdoc/ja/11-Schema.md index 87a1748da..b49b3b2ea 100644 --- a/docs/src/main/mdoc/ja/11-Schema.md +++ b/docs/src/main/mdoc/ja/11-Schema.md @@ -404,3 +404,74 @@ val user = Table[User]("user")( // CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) // ) ``` + +## カスタム データ型 + +ユーザー独自の型もしくはサポートされていない型を使用するための方法はカラムのデータ型をどのような型として扱うかを教えてあげることです。DataTypeには`mapping`メソッドが提供されているのでこのメソッドを使用して暗黙の型変換として設定します。 + +```scala 3 +case class User( + id: Long, + name: User.Name, + age: Option[Int], +) + +object User: + + case class Name(firstName: String, lastName: String) + + given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` + +LDBCでは複数のカラムをモデルが持つ1つのプロパティに統合することはできません。LDBCの目的はモデルとテーブルを1対1でマッピングを行い、データベースのテーブル定義を型安全に構築することにあるからです。 + +そのためテーブル定義とモデルで異なった数のプロパティを持つようなことは許可していません。以下のような実装はコンパイルエラーとなります。 + +```scala 3 +case class User( + id: Long, + name: User.Name, + age: Option[Int], +) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("first_name", VARCHAR(255)), + column("last_name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` + +上記のような実装を行いたい場合は以下のような実装を検討してください。 + +```scala 3 +case class User( + id: Long, + firstName: String, + lastName: String, + age: Option[Int], +): + + val name: User.Name = User.Name(firstName, lastName) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("first_name", VARCHAR(255)), + column("last_name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` From 4cf2a41731249d2bf957d2b56e5158e4ce5edd65 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:57:37 +0900 Subject: [PATCH 026/160] Fixed scalacOptions settings --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index 341a61b3d..e6b3ca933 100644 --- a/build.sbt +++ b/build.sbt @@ -197,7 +197,6 @@ lazy val benchmark = (project in file("benchmark")) lazy val docs = (project in file("docs")) .settings( description := "Documentation for ldbc", - scalacOptions := Nil, mdocIn := baseDirectory.value / "src" / "main" / "mdoc", paradoxTheme := Some(builtinParadoxTheme("generic")), paradoxProperties ++= Map( From 3738f098020cff7a00ad78347ea48055788b21db Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:57:59 +0900 Subject: [PATCH 027/160] Create 05-Program --- docs/src/main/scala/05-Program.scala | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/src/main/scala/05-Program.scala diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala new file mode 100644 index 000000000..b7190b6fa --- /dev/null +++ b/docs/src/main/scala/05-Program.scala @@ -0,0 +1,66 @@ +import scala.language.implicitConversions + +import cats.syntax.all.* + +import cats.effect.* +import cats.effect.unsafe.implicits.global + +import org.typelevel.otel4s.trace.Tracer + +import ldbc.connector.* +import ldbc.dsl.Executor +import ldbc.dsl.io.* +import ldbc.dsl.logging.LogHandler + +@main def program5(): Unit = + + // #given + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] + // #given + + // #customType + enum TaskStatus(val code: Short, val name: String): + case Pending extends TaskStatus(1, "Pending") + case Done extends TaskStatus(2, "Done") + // #customType + + // #customParameter + given Parameter[TaskStatus] with + override def bind[F[_]](statement: PreparedStatement[F], index: Int, status: TaskStatus): F[Unit] = + statement.setShort(index, status.code) + // #customParameter + + // #program1 + val program1: Executor[IO, Int] = + sql"INSERT INTO task (name, done) VALUES (${"task 1"}, ${TaskStatus.Done})".update + // #program1 + + // #customReader + given ResultSetReader[IO, TaskStatus] = + ResultSetReader.mapping[IO, Short, TaskStatus] { + case TaskStatus.Pending.code => TaskStatus.Pending + case TaskStatus.Done.code => TaskStatus.Done + } + // #customReader + + // #program2 + val program2: Executor[IO, (String, TaskStatus)] = + sql"SELECT name, done FROM task WHERE id = 1".query[(String, TaskStatus)].unsafe + // #program2 + + // #connection + def connection = Connection[IO]( + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password") + ) + // #connection + + // #run + connection.use { conn => + program1.commit(conn) *> program2.readOnly(conn).map(println(_)) + }.unsafeRunSync() + // ("task 1", Done) + // #run From c8ba3fbd38c933643c9c4629267213f4be3567f4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:58:11 +0900 Subject: [PATCH 028/160] Fixed docs programs --- docs/src/main/scala/00-Setup.scala | 4 +--- docs/src/main/scala/01-Program.scala | 5 +---- docs/src/main/scala/02-Program.scala | 4 +--- docs/src/main/scala/03-Program.scala | 2 +- docs/src/main/scala/04-Program.scala | 6 ++---- 5 files changed, 6 insertions(+), 15 deletions(-) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index 4971c55b1..de1aaadf3 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -1,5 +1,3 @@ -import cats.syntax.all.* - import cats.effect.* import cats.effect.unsafe.implicits.global @@ -45,6 +43,6 @@ import ldbc.dsl.logging.LogHandler connection.use { conn => createDatabase.commit(conn) *> conn.setSchema("todo") *> - createTable.commit(conn) + createTable.commit(conn).map(println(_)) }.unsafeRunSync() // #run diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala index 6dd4ff590..94da25fa4 100644 --- a/docs/src/main/scala/01-Program.scala +++ b/docs/src/main/scala/01-Program.scala @@ -1,5 +1,3 @@ -import cats.syntax.all.* - import cats.effect.* import cats.effect.unsafe.implicits.global @@ -7,7 +5,6 @@ import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* import ldbc.dsl.Executor -import ldbc.dsl.io.* import ldbc.dsl.logging.LogHandler @main def program1(): Unit = @@ -31,7 +28,7 @@ import ldbc.dsl.logging.LogHandler // #run connection.use { conn => - program.readOnly(conn) + program.readOnly(conn).map(println(_)) }.unsafeRunSync() // 1 // #run diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala index 826d24dfe..6673773cd 100644 --- a/docs/src/main/scala/02-Program.scala +++ b/docs/src/main/scala/02-Program.scala @@ -1,5 +1,3 @@ -import cats.syntax.all.* - import cats.effect.* import cats.effect.unsafe.implicits.global @@ -32,7 +30,7 @@ import ldbc.dsl.logging.LogHandler // #run connection.use { conn => - program.readOnly(conn) + program.readOnly(conn).map(println(_)) }.unsafeRunSync() // Some(2) // #run diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala index e0970bf05..595c6b5fc 100644 --- a/docs/src/main/scala/03-Program.scala +++ b/docs/src/main/scala/03-Program.scala @@ -37,7 +37,7 @@ import ldbc.dsl.logging.LogHandler // #run connection.use { conn => - program.readOnly(conn) + program.readOnly(conn).map(println(_)) }.unsafeRunSync() // (List(1), Some(2), 3) // #run diff --git a/docs/src/main/scala/04-Program.scala b/docs/src/main/scala/04-Program.scala index 9fd9171ef..205901964 100644 --- a/docs/src/main/scala/04-Program.scala +++ b/docs/src/main/scala/04-Program.scala @@ -1,5 +1,3 @@ -import cats.syntax.all.* - import cats.effect.* import cats.effect.unsafe.implicits.global @@ -33,7 +31,7 @@ import ldbc.dsl.logging.LogHandler // #run connection.use { conn => - program.commit(conn) + program.commit(conn).map(println(_)) }.unsafeRunSync() - // (List(1), Some(2), 3) + // 1 // #run From 507cb38743261c29b85e5c43f0e1a7750ac24060 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 7 Jul 2024 22:58:26 +0900 Subject: [PATCH 029/160] Added type alias export --- module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala index 7ff7c574b..3d76a4fb3 100644 --- a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala +++ b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala @@ -180,6 +180,13 @@ package object dsl: export ldbc.dsl.Executor export ldbc.dsl.logging.LogHandler + export ldbc.dsl.Parameter + + type ResultSetReaderIO[T] = ldbc.dsl.ResultSetReader[F, T] + export ldbc.dsl.ResultSetReader + + type PreparedStatementIO = ldbc.sql.PreparedStatement[F] + export ldbc.sql.PreparedStatement /** * Top-level imports provide aliases for the most commonly used types and modules. A typical starting set of imports From 9c813df9d92cd4389d758ec3db2731babc42db98 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 8 Jul 2024 00:33:54 +0900 Subject: [PATCH 030/160] Fixed document --- .../mdoc/ja/03-Connecting-to-a-Database.md | 1 - docs/src/main/mdoc/ja/05-Selecting-Data.md | 4 ++ docs/src/main/mdoc/ja/06-Updating-Data.md | 4 ++ docs/src/main/mdoc/ja/09-Custom-Data-Type.md | 64 +++++++++++++++++++ docs/src/main/mdoc/ja/10-Query-Builder.md | 4 ++ docs/src/main/mdoc/ja/index.md | 1 - docs/src/main/scala/05-Program.scala | 16 ++--- 7 files changed, 83 insertions(+), 11 deletions(-) diff --git a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md index f1910b331..85a7133c0 100644 --- a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md +++ b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md @@ -18,7 +18,6 @@ ldbcを使う前に、いくつかのシンボルをインポートする必要 ```scala import ldbc.dsl.io.* -import ldbc.dsl.logging.LogHandler ``` Catsも連れてこよう。 diff --git a/docs/src/main/mdoc/ja/05-Selecting-Data.md b/docs/src/main/mdoc/ja/05-Selecting-Data.md index 620d30d62..b72b42f90 100644 --- a/docs/src/main/mdoc/ja/05-Selecting-Data.md +++ b/docs/src/main/mdoc/ja/05-Selecting-Data.md @@ -28,6 +28,10 @@ services: @@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + ```shell scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala ``` diff --git a/docs/src/main/mdoc/ja/06-Updating-Data.md b/docs/src/main/mdoc/ja/06-Updating-Data.md index 33949e3b7..4cb4d7b38 100644 --- a/docs/src/main/mdoc/ja/06-Updating-Data.md +++ b/docs/src/main/mdoc/ja/06-Updating-Data.md @@ -28,6 +28,10 @@ services: @@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + ```shell scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala ``` diff --git a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md index 6caea6014..ed37ced21 100644 --- a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md +++ b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md @@ -1 +1,65 @@ # カスタム データ型 + +この章では、LDBCで構築したテーブル定義でユーザー独自の型もしくはサポートされていない型を使用するための方法について説明します。 + +@@@ vars +```yaml +version: '3' +services: + mysql: + image: mysql:"$mysqlVersion$" + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` +@@@ + +次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } + +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +``` + +## ユーザー独自の型 + +ldbcではstatementに受け渡す値を`Parameter`で表現しています。`Parameter`はstatementへのバインドする値を表現するためのtraitです。 + +`Parameter`を実装することでstatementに受け渡す値をカスタム型で表現することができます。 + +以下のコード例では、`Parameter`を実装した`CustomParameter`を定義しています。 + +@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #customType } + +@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #customParameter } + +@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #program1 } + +これでstatementにカスタム型をバインドすることができるようになりました。 + +ldbcではパラメーターの他に実行結果から独自の型を取得するための`ResultSetReader`も提供しています。 + +`ResultSetReader`を実装することでstatementの実行結果から独自の型を取得することができます。 + +以下のコード例では、`ResultSetReader`を実装した`CustomResultSetReader`を定義しています。 + +@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #customReader } + +@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #program2 } + +これでstatementの実行結果からカスタム型を取得することができるようになりました。 diff --git a/docs/src/main/mdoc/ja/10-Query-Builder.md b/docs/src/main/mdoc/ja/10-Query-Builder.md index 85caee6da..d996735b7 100644 --- a/docs/src/main/mdoc/ja/10-Query-Builder.md +++ b/docs/src/main/mdoc/ja/10-Query-Builder.md @@ -36,6 +36,10 @@ services: @@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + ```shell scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala ``` diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index e1dd3a0e1..38d4c7029 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -14,7 +14,6 @@ * [Schema Code Generation](./13-Schema-Code-Generation.md) * [Performance](./14-Perdormance.md) * [Connector OLD](./15-Connector.md) - * [Custom Data Type](./02-Custom-Data-Type.md) @@@ # ldbc (Lepus Database Connectivity) diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index b7190b6fa..002849e11 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -1,5 +1,3 @@ -import scala.language.implicitConversions - import cats.syntax.all.* import cats.effect.* @@ -20,15 +18,15 @@ import ldbc.dsl.logging.LogHandler // #given // #customType - enum TaskStatus(val code: Short, val name: String): - case Pending extends TaskStatus(1, "Pending") - case Done extends TaskStatus(2, "Done") + enum TaskStatus(val done: Boolean, val name: String): + case Pending extends TaskStatus(false, "Pending") + case Done extends TaskStatus(true, "Done") // #customType // #customParameter given Parameter[TaskStatus] with override def bind[F[_]](statement: PreparedStatement[F], index: Int, status: TaskStatus): F[Unit] = - statement.setShort(index, status.code) + statement.setBoolean(index, status.done) // #customParameter // #program1 @@ -38,9 +36,9 @@ import ldbc.dsl.logging.LogHandler // #customReader given ResultSetReader[IO, TaskStatus] = - ResultSetReader.mapping[IO, Short, TaskStatus] { - case TaskStatus.Pending.code => TaskStatus.Pending - case TaskStatus.Done.code => TaskStatus.Done + ResultSetReader.mapping[IO, Boolean, TaskStatus] { + case true => TaskStatus.Done + case false => TaskStatus.Pending } // #customReader From f531aa71a1030705e41697a2a30de257334afa20 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 8 Jul 2024 00:35:31 +0900 Subject: [PATCH 031/160] Action sbt scalafmtAll --- docs/src/main/scala/00-Setup.scala | 14 ++++++++------ docs/src/main/scala/01-Program.scala | 8 +++++--- docs/src/main/scala/02-Program.scala | 12 +++++++----- docs/src/main/scala/03-Program.scala | 8 +++++--- docs/src/main/scala/04-Program.scala | 8 +++++--- docs/src/main/scala/05-Program.scala | 14 +++++++++----- .../ldbc-dsl/src/main/scala/ldbc/dsl/package.scala | 4 ++-- 7 files changed, 41 insertions(+), 27 deletions(-) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index de1aaadf3..1e0f0c71d 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -18,7 +18,7 @@ import ldbc.dsl.logging.LogHandler // #setup val createDatabase: Executor[IO, Int] = sql"CREATE DATABASE IF NOT EXISTS todo".update - + val createTable: Executor[IO, Int] = sql""" CREATE TABLE IF NOT EXISTS `task` ( @@ -40,9 +40,11 @@ import ldbc.dsl.logging.LogHandler // #connection // #run - connection.use { conn => - createDatabase.commit(conn) *> - conn.setSchema("todo") *> - createTable.commit(conn).map(println(_)) - }.unsafeRunSync() + connection + .use { conn => + createDatabase.commit(conn) *> + conn.setSchema("todo") *> + createTable.commit(conn).map(println(_)) + } + .unsafeRunSync() // #run diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala index 94da25fa4..1a8552cf4 100644 --- a/docs/src/main/scala/01-Program.scala +++ b/docs/src/main/scala/01-Program.scala @@ -27,8 +27,10 @@ import ldbc.dsl.logging.LogHandler // #connection // #run - connection.use { conn => - program.readOnly(conn).map(println(_)) - }.unsafeRunSync() + connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() // 1 // #run diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala index 6673773cd..47fcdbee1 100644 --- a/docs/src/main/scala/02-Program.scala +++ b/docs/src/main/scala/02-Program.scala @@ -14,11 +14,11 @@ import ldbc.dsl.logging.LogHandler given Tracer[IO] = Tracer.noop[IO] given LogHandler[IO] = LogHandler.noop[IO] // #given - + // #program val program: Executor[IO, Option[Int]] = sql"SELECT 2".query[Int].to[Option] // #program - + // #connection def connection = Connection[IO]( host = "127.0.0.1", @@ -29,8 +29,10 @@ import ldbc.dsl.logging.LogHandler // #connection // #run - connection.use { conn => - program.readOnly(conn).map(println(_)) - }.unsafeRunSync() + connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() // Some(2) // #run diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala index 595c6b5fc..bc4a6f693 100644 --- a/docs/src/main/scala/03-Program.scala +++ b/docs/src/main/scala/03-Program.scala @@ -36,8 +36,10 @@ import ldbc.dsl.logging.LogHandler // #connection // #run - connection.use { conn => - program.readOnly(conn).map(println(_)) - }.unsafeRunSync() + connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() // (List(1), Some(2), 3) // #run diff --git a/docs/src/main/scala/04-Program.scala b/docs/src/main/scala/04-Program.scala index 205901964..553f28c07 100644 --- a/docs/src/main/scala/04-Program.scala +++ b/docs/src/main/scala/04-Program.scala @@ -30,8 +30,10 @@ import ldbc.dsl.logging.LogHandler // #connection // #run - connection.use { conn => - program.commit(conn).map(println(_)) - }.unsafeRunSync() + connection + .use { conn => + program.commit(conn).map(println(_)) + } + .unsafeRunSync() // 1 // #run diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index 002849e11..c8920ace7 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -1,3 +1,5 @@ +import scala.language.implicitConversions + import cats.syntax.all.* import cats.effect.* @@ -20,7 +22,7 @@ import ldbc.dsl.logging.LogHandler // #customType enum TaskStatus(val done: Boolean, val name: String): case Pending extends TaskStatus(false, "Pending") - case Done extends TaskStatus(true, "Done") + case Done extends TaskStatus(true, "Done") // #customType // #customParameter @@ -31,7 +33,7 @@ import ldbc.dsl.logging.LogHandler // #program1 val program1: Executor[IO, Int] = - sql"INSERT INTO task (name, done) VALUES (${"task 1"}, ${TaskStatus.Done})".update + sql"INSERT INTO task (name, done) VALUES (${ "task 1" }, ${ TaskStatus.Done })".update // #program1 // #customReader @@ -57,8 +59,10 @@ import ldbc.dsl.logging.LogHandler // #connection // #run - connection.use { conn => - program1.commit(conn) *> program2.readOnly(conn).map(println(_)) - }.unsafeRunSync() + connection + .use { conn => + program1.commit(conn) *> program2.readOnly(conn).map(println(_)) + } + .unsafeRunSync() // ("task 1", Done) // #run diff --git a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala index 3d76a4fb3..718a6d566 100644 --- a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala +++ b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala @@ -181,10 +181,10 @@ package object dsl: export ldbc.dsl.logging.LogHandler export ldbc.dsl.Parameter - + type ResultSetReaderIO[T] = ldbc.dsl.ResultSetReader[F, T] export ldbc.dsl.ResultSetReader - + type PreparedStatementIO = ldbc.sql.PreparedStatement[F] export ldbc.sql.PreparedStatement From 36ba76c105f174f69afc100c2287e03037e9b30b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 8 Jul 2024 20:41:04 +0900 Subject: [PATCH 032/160] Fixed docs project scalacOptions --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index e6b3ca933..6f8e6ad87 100644 --- a/build.sbt +++ b/build.sbt @@ -197,6 +197,7 @@ lazy val benchmark = (project in file("benchmark")) lazy val docs = (project in file("docs")) .settings( description := "Documentation for ldbc", + scalacOptions := Seq("-feature"), mdocIn := baseDirectory.value / "src" / "main" / "mdoc", paradoxTheme := Some(builtinParadoxTheme("generic")), paradoxProperties ++= Map( From 8e921b5addd0391e590df8f1b670d90115e76810 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 9 Jul 2024 00:33:51 +0900 Subject: [PATCH 033/160] Delete unused --- docs/src/main/scala/01-Program.scala | 3 +-- docs/src/main/scala/02-Program.scala | 2 -- docs/src/main/scala/03-Program.scala | 2 -- docs/src/main/scala/04-Program.scala | 2 -- docs/src/main/scala/05-Program.scala | 2 -- 5 files changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala index 1a8552cf4..cbcae84f5 100644 --- a/docs/src/main/scala/01-Program.scala +++ b/docs/src/main/scala/01-Program.scala @@ -4,8 +4,7 @@ import cats.effect.unsafe.implicits.global import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* -import ldbc.dsl.Executor -import ldbc.dsl.logging.LogHandler +import ldbc.dsl.io.* @main def program1(): Unit = // #given diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala index 47fcdbee1..dbb044f0c 100644 --- a/docs/src/main/scala/02-Program.scala +++ b/docs/src/main/scala/02-Program.scala @@ -4,9 +4,7 @@ import cats.effect.unsafe.implicits.global import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* -import ldbc.dsl.Executor import ldbc.dsl.io.* -import ldbc.dsl.logging.LogHandler @main def program2(): Unit = diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala index bc4a6f693..47dd51d07 100644 --- a/docs/src/main/scala/03-Program.scala +++ b/docs/src/main/scala/03-Program.scala @@ -6,9 +6,7 @@ import cats.effect.unsafe.implicits.global import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* -import ldbc.dsl.Executor import ldbc.dsl.io.* -import ldbc.dsl.logging.LogHandler @main def program3(): Unit = diff --git a/docs/src/main/scala/04-Program.scala b/docs/src/main/scala/04-Program.scala index 553f28c07..d2329aa0e 100644 --- a/docs/src/main/scala/04-Program.scala +++ b/docs/src/main/scala/04-Program.scala @@ -4,9 +4,7 @@ import cats.effect.unsafe.implicits.global import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* -import ldbc.dsl.Executor import ldbc.dsl.io.* -import ldbc.dsl.logging.LogHandler @main def program4(): Unit = diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index c8920ace7..0547abf17 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -8,9 +8,7 @@ import cats.effect.unsafe.implicits.global import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* -import ldbc.dsl.Executor import ldbc.dsl.io.* -import ldbc.dsl.logging.LogHandler @main def program5(): Unit = From 60bdba82725d7c18389605307c58e8e8ee7cfb97 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 9 Jul 2024 00:34:53 +0900 Subject: [PATCH 034/160] Addeed AutomateHeaderPlugin in docs project --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 6f8e6ad87..82c1249a0 100644 --- a/build.sbt +++ b/build.sbt @@ -217,7 +217,7 @@ lazy val docs = (project in file("docs")) connector.jvm, schema.jvm ) - .enablePlugins(MdocPlugin, SitePreviewPlugin, ParadoxSitePlugin, GhpagesPlugin, NoPublishPlugin) + .enablePlugins(MdocPlugin, SitePreviewPlugin, ParadoxSitePlugin, GhpagesPlugin, AutomateHeaderPlugin, NoPublishPlugin) lazy val ldbc = tlCrossRootProject .settings(description := "Pure functional JDBC layer with Cats Effect 3 and Scala 3") From b4257e538b5d853d340e7f7e27e0d6f2462b51e8 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 9 Jul 2024 00:35:01 +0900 Subject: [PATCH 035/160] Added auto generated header --- docs/src/main/scala/00-Setup.scala | 6 ++++++ docs/src/main/scala/01-Program.scala | 6 ++++++ docs/src/main/scala/02-Program.scala | 6 ++++++ docs/src/main/scala/03-Program.scala | 6 ++++++ docs/src/main/scala/04-Program.scala | 6 ++++++ docs/src/main/scala/05-Program.scala | 6 ++++++ 6 files changed, 36 insertions(+) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index 1e0f0c71d..88af0c547 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + import cats.effect.* import cats.effect.unsafe.implicits.global diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala index cbcae84f5..be16565e1 100644 --- a/docs/src/main/scala/01-Program.scala +++ b/docs/src/main/scala/01-Program.scala @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + import cats.effect.* import cats.effect.unsafe.implicits.global diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala index dbb044f0c..ed95036da 100644 --- a/docs/src/main/scala/02-Program.scala +++ b/docs/src/main/scala/02-Program.scala @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + import cats.effect.* import cats.effect.unsafe.implicits.global diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala index 47dd51d07..4e717c439 100644 --- a/docs/src/main/scala/03-Program.scala +++ b/docs/src/main/scala/03-Program.scala @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + import cats.syntax.all.* import cats.effect.* diff --git a/docs/src/main/scala/04-Program.scala b/docs/src/main/scala/04-Program.scala index d2329aa0e..855e402ae 100644 --- a/docs/src/main/scala/04-Program.scala +++ b/docs/src/main/scala/04-Program.scala @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + import cats.effect.* import cats.effect.unsafe.implicits.global diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index 0547abf17..91f93c433 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + import scala.language.implicitConversions import cats.syntax.all.* From 449032505c07ebb944f3cb0092a0ef596eace355 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 9 Jul 2024 09:14:56 +0900 Subject: [PATCH 036/160] Change docs Setup program --- docs/src/main/scala/00-Setup.scala | 105 +++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index 88af0c547..e796514e3 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -4,6 +4,8 @@ * For more information see LICENSE or https://opensource.org/licenses/MIT */ +import cats.syntax.all.* + import cats.effect.* import cats.effect.unsafe.implicits.global @@ -21,20 +23,85 @@ import ldbc.dsl.logging.LogHandler given LogHandler[IO] = LogHandler.noop[IO] // #given - // #setup + // #setupDatabase val createDatabase: Executor[IO, Int] = - sql"CREATE DATABASE IF NOT EXISTS todo".update + sql"CREATE DATABASE IF NOT EXISTS sandbox_db".update + // #setupDatabase + + // #setupUser + val createUser: Executor[IO, Int] = + sql""" + CREATE TABLE IF NOT EXISTS `user` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(50) NOT NULL, + `email` VARCHAR(100) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + """.update + // #setupUser + + // #setupProduct + val createProduct: Executor[IO, Int] = + sql""" + CREATE TABLE IF NOT EXISTS `product` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(100) NOT NULL, + `price` DECIMAL(10, 2) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + """.update + // #setupProduct + + // #setupOrder + val createOrder: Executor[IO, Int] = + sql""" + CREATE TABLE IF NOT EXISTS `order` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `product_id` INT NOT NULL, + `order_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `quantity` INT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES `user` (id), + FOREIGN KEY (product_id) REFERENCES `product` (id) + ) + """.update + // #setupOrder - val createTable: Executor[IO, Int] = + // #insertUser + val insertUser: Executor[IO, Int] = sql""" - CREATE TABLE IF NOT EXISTS `task` ( - `id` INT NOT NULL AUTO_INCREMENT, - `name` VARCHAR(255) NOT NULL, - `done` BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (`id`) - ) - """.update - // #setup + INSERT INTO user (name, email) VALUES + ('Alice', 'alice@example.com'), + ('Bob', 'bob@example.com'), + ('Charlie', 'charlie@example.com') + """.update + // #insertUser + + // #insertProduct + val insertProduct: Executor[IO, Int] = + sql""" + INSERT INTO product (name, price) VALUES + ('Laptop', 999.99), + ('Mouse', 19.99), + ('Keyboard', 49.99), + ('Monitor', 199.99) + """.update + // #insertProduct + + // #insertOrder + val insertOrder: Executor[IO, Int] = + sql""" + INSERT INTO `order` (user_id, product_id, quantity) VALUES + (1, 1, 1), -- Alice ordered 1 Laptop + (1, 2, 2), -- Alice ordered 2 Mice + (2, 3, 1), -- Bob ordered 1 Keyboard + (3, 4, 1) -- Charlie ordered 1 Monitor + """.update + // #insertOrder // #connection def connection = Connection[IO]( @@ -45,12 +112,24 @@ import ldbc.dsl.logging.LogHandler ) // #connection + // #setupTables + val setUpTables = + createUser *> createProduct *> createOrder + // #setupTables + + // #insertData + val insertData = + insertUser *> insertProduct *> insertOrder + // #insertData + // #run connection .use { conn => createDatabase.commit(conn) *> - conn.setSchema("todo") *> - createTable.commit(conn).map(println(_)) + conn.setSchema("sandbox_db") *> + (setUpTables *> insertData) + .transaction(conn) + .as(println("Database setup completed")) } .unsafeRunSync() // #run From fec0986bbe8f07016c59d255334918081fa053fe Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 9 Jul 2024 09:15:15 +0900 Subject: [PATCH 037/160] Added Scala CLI build file ignore settings --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4c81c834f..ada78a3ad 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ target/ .vscode/ metals.sbt +# for Scala CLI +**/.scala-build/ From 66977c175b9680b106b5e27334f9e994dd5bb267 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 9 Jul 2024 09:15:36 +0900 Subject: [PATCH 038/160] Change Setup Scala CLI command --- .../main/mdoc/ja/03-Connecting-to-a-Database.md | 16 ++++++++++++---- docs/src/main/mdoc/ja/05-Selecting-Data.md | 4 +++- docs/src/main/mdoc/ja/06-Updating-Data.md | 4 +++- docs/src/main/mdoc/ja/09-Custom-Data-Type.md | 4 +++- docs/src/main/mdoc/ja/10-Query-Builder.md | 4 +++- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md index 85a7133c0..3429d3918 100644 --- a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md +++ b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md @@ -55,9 +55,11 @@ Executorは、データベースへの接続方法、接続の受け渡し方法 このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ ## 2つめのプログラム @@ -75,9 +77,11 @@ scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-P このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ ## 3つめのプログラム @@ -93,9 +97,11 @@ scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-P このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ ## 4つめのプログラム @@ -111,6 +117,8 @@ scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-P このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ diff --git a/docs/src/main/mdoc/ja/05-Selecting-Data.md b/docs/src/main/mdoc/ja/05-Selecting-Data.md index b72b42f90..30fccc04f 100644 --- a/docs/src/main/mdoc/ja/05-Selecting-Data.md +++ b/docs/src/main/mdoc/ja/05-Selecting-Data.md @@ -32,9 +32,11 @@ services: このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ ## コレクションへの行の読み込み diff --git a/docs/src/main/mdoc/ja/06-Updating-Data.md b/docs/src/main/mdoc/ja/06-Updating-Data.md index 4cb4d7b38..8d638e3c5 100644 --- a/docs/src/main/mdoc/ja/06-Updating-Data.md +++ b/docs/src/main/mdoc/ja/06-Updating-Data.md @@ -32,9 +32,11 @@ services: このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ ## 挿入 diff --git a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md index ed37ced21..eeb820da2 100644 --- a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md +++ b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md @@ -32,9 +32,11 @@ services: このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ ## ユーザー独自の型 diff --git a/docs/src/main/mdoc/ja/10-Query-Builder.md b/docs/src/main/mdoc/ja/10-Query-Builder.md index d996735b7..ac9b51891 100644 --- a/docs/src/main/mdoc/ja/10-Query-Builder.md +++ b/docs/src/main/mdoc/ja/10-Query-Builder.md @@ -40,9 +40,11 @@ services: このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 +@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} ``` +@@@ ldbcでは、クラスを使用してクエリを構築します。 From 00a57702b3d9a86b79fb2f1f24e619ddf28e2ffd Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 10 Jul 2024 18:52:22 +0900 Subject: [PATCH 039/160] Delete unused --- docs/src/main/scala/00-Setup.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index e796514e3..906c97152 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -12,9 +12,7 @@ import cats.effect.unsafe.implicits.global import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* -import ldbc.dsl.Executor import ldbc.dsl.io.* -import ldbc.dsl.logging.LogHandler @main def setup(): Unit = From 6f05a34f3667e2fbe0f524a33ff2485d7fb4f35b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 10 Jul 2024 18:52:30 +0900 Subject: [PATCH 040/160] Create 0X-Cleanup --- docs/src/main/scala/0X-Cleanup.scala | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/src/main/scala/0X-Cleanup.scala diff --git a/docs/src/main/scala/0X-Cleanup.scala b/docs/src/main/scala/0X-Cleanup.scala new file mode 100644 index 000000000..19ec5dce4 --- /dev/null +++ b/docs/src/main/scala/0X-Cleanup.scala @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + +import cats.effect.* +import cats.effect.unsafe.implicits.global + +import org.typelevel.otel4s.trace.Tracer + +import ldbc.connector.* +import ldbc.dsl.io.* + +@main def cleanup(): Unit = + + // #given + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] + // #given + + // #cleanupDatabase + val dropDatabase: Executor[IO, Int] = + sql"DROP DATABASE IF EXISTS sandbox_db".update + // #cleanupDatabase + + // #connection + def connection = Connection[IO]( + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password") + ) + // #connection + + // #run + connection + .use { conn => + dropDatabase.commit(conn).as(println("Database dropped")) + } + .unsafeRunSync() + // #run From 1ad507357cf6c40afd95d68833b8b7ce3c6e5896 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 10 Jul 2024 19:17:02 +0900 Subject: [PATCH 041/160] Fixed mdoc index.md --- docs/src/main/mdoc/index.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md index 3aa8f2d2d..99bb635c2 100644 --- a/docs/src/main/mdoc/index.md +++ b/docs/src/main/mdoc/index.md @@ -48,35 +48,45 @@ For people that want to skip the explanations and see it action, this is the pla ### Dependency Configuration +@@@ vars ```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-dsl" % "${version}" +libraryDependencies += "$org$" %% "ldbc-dsl" % "$version$" ``` +@@@ For Cross-Platform projects (JVM, JS, and/or Native): +@@@ vars ```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-dsl" % "${version}" +libraryDependencies += "$org$" %%% "ldbc-dsl" % "$version$" ``` +@@@ The dependency package used depends on whether the database connection is made via a connector using the Java API or a connector provided by ldbc. **Use jdbc connector** +@@@ vars ```scala -libraryDependencies += "io.github.takapi327" %% "jdbc-connector" % "${version}" +libraryDependencies += "$org$" %% "jdbc-connector" % "$version$" ``` +@@@ **Use ldbc connector** +@@@ vars ```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-connector" % "${version}" +libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" ``` +@@@ For Cross-Platform projects (JVM, JS, and/or Native) +@@@ vars ```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-connector" % "${version}" +libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" ``` +@@@ ### Usage @@ -172,15 +182,19 @@ ldbc also allows type-safe construction of schema information for tables. The first step is to set up dependencies. +@@@ vars ```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-schema" % "${version}" +libraryDependencies += "$org$" %% "ldbc-schema" % "$version$" ``` +@@@ For Cross-Platform projects (JVM, JS, and/or Native): +@@@ vars ```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-schema" % "${version}" +libraryDependencies += "$org$" %%% "ldbc-schema" % "$version$" ``` +@@@ The next step is to create a schema for use by the query builder. From 05c7faed34165a29f45abf58ac20e86ba19bf8cc Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 10 Jul 2024 19:17:16 +0900 Subject: [PATCH 042/160] Change comment --- docs/src/main/scala/00-Setup.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index 906c97152..ffd806360 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -110,10 +110,10 @@ import ldbc.dsl.io.* ) // #connection - // #setupTables + // #setupTable val setUpTables = createUser *> createProduct *> createOrder - // #setupTables + // #setupTable // #insertData val insertData = From b6da1dee9b140da60799d13b537a11f387c5d0f8 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 10 Jul 2024 19:17:26 +0900 Subject: [PATCH 043/160] Change setup docs --- .../mdoc/ja/03-Connecting-to-a-Database.md | 71 ++++++++++++++++++- docs/src/main/mdoc/ja/05-Selecting-Data.md | 1 - docs/src/main/mdoc/ja/06-Updating-Data.md | 1 - docs/src/main/mdoc/ja/09-Custom-Data-Type.md | 1 - docs/src/main/mdoc/ja/10-Query-Builder.md | 1 - 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md index 3429d3918..e8f360cce 100644 --- a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md +++ b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md @@ -4,11 +4,76 @@ ## セットアップ -まず、`build.sbt`に依存関係を追加します。 +まず、データベースを起動します。以下のコードを使用して、データベースを起動します。 @@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-dsl" % "$version$" +```yaml +version: '3' +services: + mysql: + image: mysql:"$mysqlVersion$" + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` +@@@ + + +次に、データベースの初期化を行います。 + +以下コードのようにデータベースの作成を行います。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupDatabase } + +次に、テーブルの作成を行います。 + +ここでは「ユーザー(user)」、「注文(order)」、「製品(product)」の3つのテーブルを使用した各ユーザーが複数の注文を行い、各注文が特定の製品に関連付けられている状況をシミュレートします。 + +**ユーザー(user)** + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupUser } + +**注文(order)** + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupProduct } + +**製品(product)** + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupOrder } + +それぞれのテーブルにデータを挿入します。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertUser } +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertProduct } +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertOrder } + +そしてデータベースに接続するためのコネクタを作成する。コネクタは、データベースへの接続を管理するためのリソースである。コネクタは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #connection } + +最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 + +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupTable } +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertData } +@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #run } + +**Scala CLIで実行** + +このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +@@@ vars +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:$version$ --dependency io.github.takapi327::ldbc-connector:$version$ ``` @@@ diff --git a/docs/src/main/mdoc/ja/05-Selecting-Data.md b/docs/src/main/mdoc/ja/05-Selecting-Data.md index 30fccc04f..a275ce7f0 100644 --- a/docs/src/main/mdoc/ja/05-Selecting-Data.md +++ b/docs/src/main/mdoc/ja/05-Selecting-Data.md @@ -26,7 +26,6 @@ services: 次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } **Scala CLIで実行** diff --git a/docs/src/main/mdoc/ja/06-Updating-Data.md b/docs/src/main/mdoc/ja/06-Updating-Data.md index 8d638e3c5..783010555 100644 --- a/docs/src/main/mdoc/ja/06-Updating-Data.md +++ b/docs/src/main/mdoc/ja/06-Updating-Data.md @@ -26,7 +26,6 @@ services: 次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } **Scala CLIで実行** diff --git a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md index eeb820da2..459c098ad 100644 --- a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md +++ b/docs/src/main/mdoc/ja/09-Custom-Data-Type.md @@ -26,7 +26,6 @@ services: 次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } **Scala CLIで実行** diff --git a/docs/src/main/mdoc/ja/10-Query-Builder.md b/docs/src/main/mdoc/ja/10-Query-Builder.md index ac9b51891..87104830e 100644 --- a/docs/src/main/mdoc/ja/10-Query-Builder.md +++ b/docs/src/main/mdoc/ja/10-Query-Builder.md @@ -34,7 +34,6 @@ services: 次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setup } **Scala CLIで実行** From 9d6dde656552df982c9ae3e51ece42720da00b0a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 13 Jul 2024 09:54:47 +0900 Subject: [PATCH 044/160] Fixed index.md --- docs/src/main/mdoc/index.md | 92 +++++++++---------------------------- 1 file changed, 21 insertions(+), 71 deletions(-) diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md index 99bb635c2..cd75c91c0 100644 --- a/docs/src/main/mdoc/index.md +++ b/docs/src/main/mdoc/index.md @@ -18,10 +18,6 @@ ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effec ldbc is a [Typelevel](http://typelevel.org/) project. This means we embrace pure, typeful, functional programming, and provide a safe and friendly environment for teaching, learning, and contributing as described in the Scala [Code of Conduct](http://scala-lang.org/conduct.html). -ldbc is Created under the influence of [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. Using tapir, you can build type-safe endpoints and also generate OpenAPI documentation from the endpoints you build. - -ldbc allows the same type-safe construction with Scala at the database layer and document generation using the constructed one. - Note that **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. ## Modules availability @@ -141,37 +137,43 @@ val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn => ldbc provides not only plain queries but also type-safe database connections using the query builder. -The first step is to create a schema for use by the query builder. +The first step is to set up dependencies. -ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in definition order. Table definitions are very similar to the structure of Create statements. This makes the construction of table definitions intuitive for the user. +```scala +libraryDependencies += "io.github.takapi327" %% "ldbc-query-builder" % "${version}" +``` + +For Cross-Platform projects (JVM, JS, and/or Native): + +```scala +libraryDependencies += "io.github.takapi327" %%% "ldbc-query-builder" % "${version}" +``` + +ldbc uses classes to construct queries. ```scala +import ldbc.query.builder.Table + case class User( id: Long, name: String, age: Option[Int], -) - -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) +) derives Table ``` -The next step is to build a TableQuery using the schema you have created. +The next step is to create a Table using the classes you have created. ```scala -import ldbc.query.builder.TableQuery +import ldbc.query.builder.Table -val userQuery = TableQuery[User](table) +val userTable = Table[User] ``` Finally, you can use the query builder to create a query. ```scala val result: IO[List[User]] = connection.use { conn => - userQuery.selectAll.toList[User].readOnly(conn) + userTable.selectAll.query[User].to[List].readOnly(conn) // "SELECT `id`, `name`, `age` FROM user" } ``` @@ -182,19 +184,15 @@ ldbc also allows type-safe construction of schema information for tables. The first step is to set up dependencies. -@@@ vars ```scala -libraryDependencies += "$org$" %% "ldbc-schema" % "$version$" +libraryDependencies += "io.github.takapi327" %% "ldbc-schema" % "${version}" ``` -@@@ For Cross-Platform projects (JVM, JS, and/or Native): -@@@ vars ```scala -libraryDependencies += "$org$" %%% "ldbc-schema" % "$version$" +libraryDependencies += "io.github.takapi327" %%% "ldbc-schema" % "${version}" ``` -@@@ The next step is to create a schema for use by the query builder. @@ -232,54 +230,6 @@ Full documentation can be found at Currently available in English and Japanese. - [English](/ldbc/en/index.html) - [Japanese](/ldbc/ja/index.html) -## Features/Roadmap - -Creating a MySQL connector project written in pure Scala3. - -JVM, JS and Native platforms are all supported. - -> [!IMPORTANT] -> **ldbc** is currently focused on developing connectors written in pure Scala3 to work with JVM, JS and Native. -> In the future, we also plan to rewrite existing functions based on a pure Scala3 connector. - -### Enhanced functionality and improved stability of the MySQL connector written in pure Scala3 - -Most of the jdbc functionality used in other packages of ldbc at the moment could be implemented. - -However, not all jdbc APIs could be supported. Nor can we guarantee that it is proven and stable enough to operate in a production environment. - -We will continue to develop features and improve the stability of the ldbc connector to achieve the same level of stability and reliability as the jdbc connector. - -#### Connection pooling implementation - -- [ ] Failover Countermeasures - -#### Performance Verification - -- [ ] Comparison with JDBC -- [ ] Comparison with other MySQL Scala libraries -- [ ] Verification of operation in AWS and other infrastructure environments - -#### Other - -- [ ] Additional streaming implementation -- [ ] Integration with java.sql API -- [ ] etc... - -### Redesign of query builders and schema definitions - -Initially, ldbc was inspired by tapir to create a development system that could centralise Scala models, sql schemas and documentation by managing a single resource at the database level. - -In addition, database connection, query construction and document generation were to be used in combination with retrofitted packages, as the aim was to be able to integrate with other database systems. - -As a result, we feel that it has become difficult for users to use because of the various configurations required to build it. - -What users originally wanted from a database connectivity library was something simpler, easier and more intuitive to use. - -Initially, ldbc aimed to create documentation from the schema, so building the schema and query builder was not as simple as it could have been, as it required a complete description of the database data types and so on. - -It was therefore decided to redesign it to make it simpler and easier to use. - ## Contributing All suggestions welcome :)! From f0b170d0fd0c9cd32eceabadba57be9ce4e8d211 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 13 Jul 2024 10:34:03 +0900 Subject: [PATCH 045/160] Added Migration Notes document --- docs/src/main/mdoc/ja/01-Migration-Notes.md | 239 ++++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/docs/src/main/mdoc/ja/01-Migration-Notes.md b/docs/src/main/mdoc/ja/01-Migration-Notes.md index 923caaefa..9ff54a336 100644 --- a/docs/src/main/mdoc/ja/01-Migration-Notes.md +++ b/docs/src/main/mdoc/ja/01-Migration-Notes.md @@ -1,3 +1,242 @@ # 移行ノート ## Upgrading to 0.2.x from 0.3.x + +### Packages + +**パッケージ名の変更** + +| 0.2.x | 0.3.x | +|-----------|-------------| +| ldbc-core | ldbc-schema | + +**新規パッケージ** + +新たに2種類のパッケージが追加されました。 + +| Module / Platform | JVM | Scala Native | Scala.js | +|----------------------|:---:|:------------:|:--------:| +| `ldbc-connector` | ✅ | ✅ | ✅ | +| `jdbc-connector` | ✅ | ❌ | ❌ | + +**全てのパッケージ** + +| Module / Platform | JVM | Scala Native | Scala.js | +|----------------------|:---:|:------------:|:--------:| +| `ldbc-sql` | ✅ | ✅ | ✅ | +| `ldbc-connector` | ✅ | ✅ | ✅ | +| `jdbc-connector` | ✅ | ❌ | ❌ | +| `ldbc-dsl` | ✅ | ✅ | ✅ | +| `ldbc-query-builder` | ✅ | ✅ | ✅ | +| `ldbc-schema` | ✅ | ✅ | ✅ | +| `ldbc-schemaSpy` | ✅ | ❌ | ❌ | +| `ldbc-codegen` | ✅ | ✅ | ✅ | +| `ldbc-hikari` | ✅ | ❌ | ❌ | +| `ldbc-plugin` | ✅ | ❌ | ❌ | + +### 機能変更 + +#### コネクタ切り替え機能 + +Scala MySQL コネクタに、JDBC と ldbc の接続切り替えのサポートが追加されました。 + +この変更により、開発者はプロジェクトの要件に応じて JDBC または ldbc ライブラリを使用したデータベース接続を柔軟に選択できるようになりました。これにより、開発者は異なるライブラリの機能を利用できるようになり、接続の設定や操作の柔軟性が向上します。 + +##### 変更方法 + +まず、共通の依存関係を設定する。 + +@@@ vars +```scala 3 +libraryDependencies += "$org$" %% "ldbc-dsl" % "$version$" +``` +@@@ + +クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ) + +@@@ vars +```scala 3 +libraryDependencies += "$org$" %%% "ldbc-dsl" % "$version$" +``` +@@@ + +使用される依存パッケージは、データベース接続が Java API を使用するコネクタを介して行われるか、または ldbc によって提供されるコネクタを介して行われるかによって異なります。 + +**jdbcコネクタの使用** + +@@@ vars +```scala 3 +libraryDependencies += "$org$" %% "jdbc-connector" % "$version$" +``` +@@@ + +**ldbcコネクタの使用** + +@@@ vars +```scala 3 +libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" +``` +@@@ + +クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ) + +@@@ vars +```scala 3 +libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" +``` +@@@ + +##### 使用方法 + +**jdbcコネクタの使用** + +```scala 3 +val ds = new com.mysql.cj.jdbc.MysqlDataSource() +ds.setServerName("127.0.0.1") +ds.setPortNumber(13306) +ds.setDatabaseName("world") +ds.setUser("ldbc") +ds.setPassword("password") + +val datasource = jdbc.connector.MysqlDataSource[IO](ds) + +val connection: Resource[IO, Connection[IO]] = + Resource.make(datasource.getConnection)(_.close()) +``` + +**ldbcコネクタの使用** + +```scala 3 +val connection: Resource[IO, Connection[IO]] = + ldbc.connector.Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password"), + database = Some("ldbc"), + ssl = SSL.Trusted + ) +``` + +データベースへの接続処理は、それぞれの方法で確立されたコネクションを使って行うことができる。 + +```scala 3 +val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn => + (for + result1 <- sql"SELECT 1".query[Int].to[List] + result2 <- sql"SELECT 2".query[Int].to[Option] + result3 <- sql"SELECT 3".query[Int].unsafe + yield (result1, result2, result3)).readOnly(conn) +} +``` + +### 破壊的変更 + +#### プレーン・クエリ構築の拡張 + +プレーン・クエリを用いたデータベース接続メソッドによる検索対象の型の決定は、検索対象の型とそのフォーマット(リストまたはオプション)を一括して指定していた。 + +今回の修正ではこれを変更し、取得する型とその形式の指定を分離することで内部ロジックを共通化した。これにより、プレーン・クエリの構文はよりdoobieに近くなり、doobieのユーザは混乱することなく使用できるはずである。 + +**before** + +```scala 3 +sql"SELECT id, name, age FROM user".toList[(Long, String, Int)].readOnly(connection) +sql"SELECT id, name, age FROM user WHERE id = ${1L}".headOption[User].readOnly(connection) +``` + +**after** + +```scala 3 +sql"SELECT id, name, age FROM user".query[(Long, String, Int)].to[List].readOnly(connection) +sql"SELECT id, name, age FROM user WHERE id = ${1L}".query[User].to[Option].readOnly(connection) +``` + +#### AUTO INCREMENT値取得メソッド命名変更 + +更新 API で AUTO INCREMENT 列によって生成された値を変換する API `updateReturningAutoGeneratedKey` の名前が `returning` に変更されました。 + +これはMySQLの特徴で、MySQLはデータ挿入時にAUTO INCREMENTで生成された値を返しますが、他のRDBは動作が異なり、AUTO INCREMENTで生成された値以外の値を返すことがあります。 +API 名は、将来の拡張を考慮して、限定的な API 名をより拡張しやすくするために早い段階で変更されました。 + +**before** + +```scala 3 +sql"INSERT INTO `table`(`id`, `c1`) VALUES ($None, ${ "column 1" })".updateReturningAutoGeneratedKey[Long] +``` + +**after** + +```scala 3 +sql"INSERT INTO `table`(`id`, `c1`) VALUES ($None, ${ "column 1" })".returning[Long] +``` + +#### クエリビルダーの構築方法 + +以前まではクエリビルダーはテーブルスキーマを構築しなければ使用することができませんでした。 + +今回の更新で、より簡易的にクエリビルダーを使用できるように変更を行いました。 + +**before** + +まずモデルに対応したテーブルスキーマを作成し、 + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val userTable = Table[User]("user")( // CREATE TABLE `user` ( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, + column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL +) +``` + +次にテーブルスキーマを使用して`TableQuery`の構築を行います。 + +```scala 3 +val tableQuery = TableQuery[IO, User](userTable) +``` + +最後にクエリ構築を行っていました。 + +```scala 3 +val result: IO[List[User]] = connection.use { conn => + tableQuery.selectAll.toList[User].readOnly(conn) + // "SELECT `id`, `name`, `age` FROM user" +} +``` + +**after** + +今回の変更によって、モデルを構築し + +```scala 3 +import ldbc.query.builder.Table + +case class User( + id: Long, + name: String, + age: Option[Int], +) derives Table +``` + +次に`Table`を初期化を行います。 + +```scala 3 +import ldbc.query.builder.Table + +val userTable = Table[User] +``` + +最後にクエリ構築を行うことで利用可能となります。 + +```scala +val result: IO[List[User]] = connection.use { conn => + userTable.selectAll.query[User].to[List].readOnly(conn) + // "SELECT `id`, `name`, `age` FROM user" +} +``` From 5de2b5a5b367a85b7ffb4c8aeaa85b26a4f5306f Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 14 Jul 2024 19:43:45 +0900 Subject: [PATCH 046/160] Fixed migrate title --- docs/src/main/mdoc/ja/01-Migration-Notes.md | 2 +- docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/main/mdoc/ja/01-Migration-Notes.md b/docs/src/main/mdoc/ja/01-Migration-Notes.md index 9ff54a336..c225b9091 100644 --- a/docs/src/main/mdoc/ja/01-Migration-Notes.md +++ b/docs/src/main/mdoc/ja/01-Migration-Notes.md @@ -1,6 +1,6 @@ # 移行ノート -## Upgrading to 0.2.x from 0.3.x +## Upgrading to 0.3.x from 0.2.x ### Packages diff --git a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md index e8f360cce..e5de2664d 100644 --- a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md +++ b/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md @@ -28,7 +28,6 @@ services: ``` @@@ - 次に、データベースの初期化を行います。 以下コードのようにデータベースの作成を行います。 From 1b596708f9692bd5b171a47db027e1d0a01d0110 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:20:42 +0900 Subject: [PATCH 047/160] Added sbt-typelevel-site plugin --- docs/src/main/mdoc/en/01-Table-Definitions.md | 406 --------- docs/src/main/mdoc/en/02-Custom-Data-Type.md | 78 -- .../mdoc/en/03-Type-safe-Query-Builder.md | 486 ---------- .../main/mdoc/en/04-Database-Connection.md | 410 --------- docs/src/main/mdoc/en/05-Plain-SQL-Queries.md | 34 - .../06-Generating-SchemaSPY-Documentation.md | 76 -- .../main/mdoc/en/07-Schema-Code-Generation.md | 202 ----- docs/src/main/mdoc/en/08-Perdormance.md | 39 - docs/src/main/mdoc/en/09-Connector.md | 848 ------------------ docs/src/main/mdoc/en/index.md | 127 --- docs/src/main/mdoc/img/compile_create.png | Bin 48533 -> 0 bytes .../main/mdoc/img/compile_create_query.png | Bin 47649 -> 0 bytes docs/src/main/mdoc/img/insert_throughput.png | Bin 53539 -> 0 bytes docs/src/main/mdoc/img/lepus_logo.png | Bin 11636 -> 0 bytes docs/src/main/mdoc/img/runtime_create.png | Bin 42249 -> 0 bytes .../main/mdoc/img/runtime_create_query.png | Bin 50016 -> 0 bytes docs/src/main/mdoc/img/select_throughput.png | Bin 50666 -> 0 bytes docs/src/main/mdoc/index.md | 277 ------ docs/src/main/mdoc/ja/01-Table-Definitions.md | 406 --------- docs/src/main/mdoc/ja/02-Custom-Data-Type.md | 78 -- .../mdoc/ja/03-Type-safe-Query-Builder.md | 486 ---------- .../main/mdoc/ja/04-Database-Connection.md | 410 --------- docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md | 34 - .../06-Generating-SchemaSPY-Documentation.md | 76 -- .../main/mdoc/ja/07-Schema-Code-Generation.md | 202 ----- docs/src/main/mdoc/ja/08-Perdormance.md | 39 - docs/src/main/mdoc/ja/09-Connector.md | 842 ----------------- docs/src/main/mdoc/ja/index.md | 127 --- project/plugins.sbt | 4 +- 29 files changed, 1 insertion(+), 5686 deletions(-) delete mode 100644 docs/src/main/mdoc/en/01-Table-Definitions.md delete mode 100644 docs/src/main/mdoc/en/02-Custom-Data-Type.md delete mode 100644 docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md delete mode 100644 docs/src/main/mdoc/en/04-Database-Connection.md delete mode 100644 docs/src/main/mdoc/en/05-Plain-SQL-Queries.md delete mode 100644 docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md delete mode 100644 docs/src/main/mdoc/en/07-Schema-Code-Generation.md delete mode 100644 docs/src/main/mdoc/en/08-Perdormance.md delete mode 100644 docs/src/main/mdoc/en/09-Connector.md delete mode 100644 docs/src/main/mdoc/en/index.md delete mode 100644 docs/src/main/mdoc/img/compile_create.png delete mode 100644 docs/src/main/mdoc/img/compile_create_query.png delete mode 100644 docs/src/main/mdoc/img/insert_throughput.png delete mode 100644 docs/src/main/mdoc/img/lepus_logo.png delete mode 100644 docs/src/main/mdoc/img/runtime_create.png delete mode 100644 docs/src/main/mdoc/img/runtime_create_query.png delete mode 100644 docs/src/main/mdoc/img/select_throughput.png delete mode 100644 docs/src/main/mdoc/index.md delete mode 100644 docs/src/main/mdoc/ja/01-Table-Definitions.md delete mode 100644 docs/src/main/mdoc/ja/02-Custom-Data-Type.md delete mode 100644 docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md delete mode 100644 docs/src/main/mdoc/ja/04-Database-Connection.md delete mode 100644 docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md delete mode 100644 docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md delete mode 100644 docs/src/main/mdoc/ja/07-Schema-Code-Generation.md delete mode 100644 docs/src/main/mdoc/ja/08-Perdormance.md delete mode 100644 docs/src/main/mdoc/ja/09-Connector.md delete mode 100644 docs/src/main/mdoc/ja/index.md diff --git a/docs/src/main/mdoc/en/01-Table-Definitions.md b/docs/src/main/mdoc/en/01-Table-Definitions.md deleted file mode 100644 index 50e44f372..000000000 --- a/docs/src/main/mdoc/en/01-Table-Definitions.md +++ /dev/null @@ -1,406 +0,0 @@ -# Table Definitions - -This chapter describes how to work with database schemas in Scala code, especially how to manually write a schema, which is useful when starting to write an application without an existing database. If you already have a schema in your database, you can skip this step using the [code generator](/ldbc/en/07-Schema-Code-Generation.html). - -The following code example assumes the following import - -```scala 3 -import ldbc.core.* -import ldbc.core.attribute.* -``` - -LDBC maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in the order of definition. Table definitions are very similar to the structure of a Create statement. This makes the construction of table definitions intuitive for the user. - -LDBC uses this table definition for a variety of purposes Generating type-safe queries, generating documents, etc. - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) // ); -``` - -All columns are defined by the column method. Each column has a column name, data type, and attributes. The following primitive types are supported by default and are ready to use - -- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` -- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` -- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` -- String -- Boolean -- java.time.* - -Nullable columns are represented by Option[T], where T is one of the supported primitive types; note that any column that is not of type Option is Not Null. - -## Data Type - -The mapping of the Scala type of a property that a model has to the data type that a column has requires that the defined data type supports the Scala type. Attempting to assign an unsupported type will result in a compile error. - -The following table shows the Scala types supported by the data types. - -| Data Type | Scala Type | -|------------|-----------------------------------------------------------------------------------------------| -| BIT | Byte, Short, Int, Long | -| TINYINT | Byte, Short | -| SMALLINT | Short, Int | -| MEDIUMINT | Int | -| INT | Int, Long | -| BIGINT | Long, BigInt | -| DECIMAL | BigDecimal | -| FLOAT | Float | -| DOUBLE | Double | -| CHAR | String | -| VARCHAR | String | -| BINARY | Array[Byte] | -| VARBINARY | Array[Byte] | -| TINYBLOB | Array[Byte] | -| BLOB | Array[Byte] | -| MEDIUMBLOB | Array[Byte] | -| LONGBLOB | Array[Byte] | -| TINYTEXT | String | -| TEXT | String | -| MEDIUMTEXT | String | -| DATE | java.time.LocalDate | -| DATETIME | java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime | -| TIMESTAMP | java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime | -| TIME | java.time.LocalTime | -| YEAR | java.time.Instant, java.time.LocalDate, java.time.Year | -| BOOLEAN | Boolean | - -**Points to keep in mind when dealing with integer types** - -It should be noted that the range of data that can be handled, depending on whether it is signed or unsigned, does not fit within the Scala types. - -| Data Type | signed range | unsigned range | Scala Type | range | -|-----------|--------------------------------------------|--------------------------|----------------|--------------------------------------------------------------------| -| TINYINT | -128 ~ 127 | 0 ~ 255 | Byte
Short | -128 ~ 127
-32768~32767 | -| SMALLINT | -32768 ~ 32767 | 0 ~ 65535 | Short
Int | -32768~32767
-2147483648~2147483647 | -| MEDIUMINT | -8388608 ~ 8388607 | 0 ~ 16777215 | Int | -2147483648~2147483647 | -| INT | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | Int
Long | -2147483648~2147483647
-9223372036854775808~9223372036854775807 | -| BIGINT | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 18446744073709551615 | Long
BigInt | -9223372036854775808~9223372036854775807
... | - -To work with user-defined proprietary or unsupported types, see [Custom Types](/ldbc/en/02-Custom-Data-Type.html). - -## Attribute - -Various attributes can be assigned to columns. - -- `AUTO_INCREMENT` - Mark columns as auto-increment keys when creating DDL statements and documenting SchemaSPY. - MySQL cannot return columns that are not AutoInc when inserting data. Therefore, if necessary, LDBC will check to see if the return column is properly marked as AutoInc. -- `PRIMARY_KEY` - Mark columns as primary keys when creating DDL statements and SchemaSPY documents. -- `UNIQUE_KEY` - Mark columns as unique keys when creating DDL statements and SchemaSPY documents. -- `COMMENT` - Set comments on columns when creating DDL statements and SchemaSPY documents. - -## Key Settings - -MySQL allows you to set various keys for tables, such as Unique keys, Index keys, foreign keys, etc. Let's look at how to set these keys in a table definition built with LDBC. - -### PRIMARY KEY - -A primary key is an item that uniquely identifies data in MySQL. When a primary key constraint is set on a column, the column can only contain values that do not duplicate the values of other data. It also cannot contain NULLs. As a result, only one piece of data in the table can be identified by searching for a value in a column with a primary key constraint. - -LDBC allows this primary key constraint to be set in two ways. - -1. set as an attribute of column method -2. set by keySet method of table - -**Set as an attribute of the column method** - -It is very easy to set a column method as an attribute by simply passing `PRIMARY_KEY` as the third or later argument of the column method. This allows you to set the `id` column as the primary key in the following cases - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) -``` - -**Set by keySet method of table** - -LDBC table definitions have a method called `keySet`, where you can set a column as a primary key by passing `PRIMARY_KEY` as the column to be set as the primary key. - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => PRIMARY_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`) -// ) -``` - -The `PRIMARY_KEY` method accepts the following parameters in addition to columns - -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### Compound key (primary key) - -Not only one column, but also multiple columns can be set as a combined primary key. You can set multiple columns as primary keys by simply passing multiple columns to `PRIMARY_KEY` as primary keys. - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => PRIMARY_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`, `name`) -// ) -``` - -Compound keys can only be set with `PRIMARY_KEY` in the `keySet` method. If multiple keys are set as attributes of the column method as shown below, each will be set as a primary key, not as a compound key. - -LDBC does not allow multiple `PRIMARY_KEY`s in a table definition to cause a compile error. However, if the table definition is used in query generation, document generation, etc., an error will occur. This is due to the restriction that only one PRIMARY KEY can be set per table. - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255), PRIMARY_KEY), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - -// CREATE TABLE `user` ( -// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, -// ) -``` - -### UNIQUE KEY - -A unique key is an item that uniquely identifies data in MySQL. When a column has a uniqueness constraint, it can only contain values that do not duplicate the values of other data. - -LDBC allows this uniqueness constraint to be set in two ways. - -1. set as an attribute of column method -2. set by keySet method of table - -**Set as an attribute of the column method** - -It is very easy to set a column method as an attribute by simply passing `UNIQUE_KEY` as the third or later argument of the column method. This allows you to set the `id` column as a unique key in the following cases - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) -``` - -**Set by keySet method of table** - -LDBC table definitions have a method called `keySet`, where you can set a column as a unique key by passing the column you want to set as a unique key to `UNIQUE_KEY`. - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => UNIQUE_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`) -// ) -``` - -The `UNIQUE_KEY` method accepts the following parameters in addition to columns - -- `Index Name` String -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### Compound key (unique key) - -You can set not only one column but also multiple columns as a unique key as a combined unique key. You can set multiple columns as unique keys by simply passing `UNIQUE_KEY` with the columns you want to set as unique keys. - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => UNIQUE_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`, `name`) -// ) -``` - -Compound keys can only be set with `UNIQUE_KEY` in the `keySet` method. If you set multiple keys as attributes in the column method, each will be set as a unique key, not as a compound key. - -### INDEX KEY - -An index key is an "index" in MySQL to efficiently retrieve the desired record. - -LDBC allows this index to be set in two ways. - -1. set as an attribute of column method -2. set by keySet method of table - -**Set as an attribute of the column method** - -It is very easy to set a column method as an attribute, just pass `INDEX_KEY` as the third argument or later of the column method. This allows you to set the `id` column as an index in the following cases - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) -``` - -**Set by keySet method of table** - -LDBC table definitions have a method called `keySet`, where you can set a column as an index key by passing the column you want to set as an index to `INDEX_KEY`. - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => INDEX_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`) -// ) -``` - -The `INDEX_KEY` method accepts the following parameters in addition to columns - -- `Index Name` String -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### Compound key (index key) - -You can set not only one column but also multiple columns as index keys as a combined index key. You can set up a composite index by simply passing multiple columns as index keys to `INDEX_KEY`. - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => INDEX_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`, `name`) -// ) -``` - -Compound keys can only be set with `INDEX_KEY` in the `keySet` method. If you set multiple columns as attributes of the `column` method, they will each be set as an index key, not as a composite index. - -### FOREIGN KEY - -A foreign key is a data integrity constraint (referential integrity constraint) in MySQL. A column set to a foreign key can only have values that exist in the columns of the referenced table. - -In LDBC, this foreign key constraint can be set by using the keySet method of table. - -```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)) -) - -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) -// ) -``` - -The `FOREIGN_KEY` method accepts the following parameters in addition to column and reference values. - -- `Index Name` String - -Foreign key constraints can be used to set the behavior of the parent table on delete and update. The `REFERENCE` method provides the `onDelete` and `onUpdate` methods, which can be used to set the respective behavior. - -Values that can be set can be obtained from `ldbc.core.Reference.ReferenceOption`. - -```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id).onDelete(Reference.ReferenceOption.RESTRICT))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT -// ) -``` - -The values that can be set are as follows - -- `RESTRICT`: Deny delete or update operations on parent tables. -- `CASCADE`: Deletes or updates rows from the parent table and automatically deletes or updates matching rows in the child tables. -- `SET_NULL`: Deletes or updates rows from the parent table and sets foreign key columns in the child table to NULL. -- `NO_ACTION`: Standard SQL keywords. In MySQL, equivalent to RESTRICT. -- `SET_DEFAULT`: This action is recognized by the MySQL parser, but both InnoDB and NDB will reject table definitions containing an ON DELETE SET DEFAULT or ON UPDATE SET DEFAULT clause. - -#### Compound key (foreign key) - -Not only one column, but also multiple columns can be combined as a foreign key. Simply pass multiple columns to `FOREIGN_KEY` to be set as foreign keys as a compound foreign key. - -```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("category", SMALLINT[Short]) -) - -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]), - column("post_category", SMALLINT[Short]) -) - .keySet(table => FOREIGN_KEY((table.postId, table.postCategory), REFERENCE(post, (post.id, post.category)))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`, `post_category`) REFERENCES `post` (`id`, `category`) -// ) -``` - -### Constraint Name - -MySQL allows you to give arbitrary names to constraints by using CONSTRAINT. The constraint name must be unique on a per-database basis. - -LDBC provides the CONSTRAINT method, so the process of setting constraints such as key constraints can be set by simply passing the process to the CONSTRAINT method. - -```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => CONSTRAINT("fk_post_id", FOREIGN_KEY(table.postId, REFERENCE(post, post.id)))) - -// CREATE TABLE `user` ( -// ..., -// CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) -// ) -``` diff --git a/docs/src/main/mdoc/en/02-Custom-Data-Type.md b/docs/src/main/mdoc/en/02-Custom-Data-Type.md deleted file mode 100644 index 21b9da4c3..000000000 --- a/docs/src/main/mdoc/en/02-Custom-Data-Type.md +++ /dev/null @@ -1,78 +0,0 @@ -# Custom Data Type - -This chapter describes how to use user-specific or unsupported types in table definitions built with LDBC. - -The following code example assumes the following import - -```scala 3 -import ldbc.core.* -``` - -The way to use user-specific or unsupported types is to tell the column data type what type to treat as a data type; DataType provides a `mapping` method that can be used to set this up as an implicit type conversion. - -```scala 3 -case class User( - id: Long, - name: User.Name, - age: Option[Int], -) - -object User: - - case class Name(firstName: String, lastName: String) - - given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` - -LDBC does not allow multiple columns to be merged into a single property of a model, since the purpose of LDBC is to provide a one-to-one mapping between models and tables, and to construct database table definitions in a type-safe manner. - -Therefore, it is not allowed to have different number of properties in the table definition and in the model. The following implementation will result in a compile error - -```scala 3 -case class User( - id: Long, - name: User.Name, - age: Option[Int], -) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` - -If you wish to implement the above, please consider the following implementation. - -```scala 3 -case class User( - id: Long, - firstName: String, - lastName: String, - age: Option[Int], -): - - val name: User.Name = User.Name(firstName, lastName) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` diff --git a/docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md b/docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md deleted file mode 100644 index 3b7ea191a..000000000 --- a/docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md +++ /dev/null @@ -1,486 +0,0 @@ -# Type-safe Query Construction - -This chapter describes how to use LDBC-built table definitions to construct type-safe queries. - -The following dependencies must be set up for the project - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-query-builder" % "$version$" -``` -@@@ - -If you have not yet read how to define tables in LDBC, we recommend that you read the chapter [Table Definitions](/ldbc/en/01-Table-Definitions.html) first. - -The following code example assumes the following import - -```scala 3 -import cats.effect.IO -import ldbc.core.* -import ldbc.query.builder.TableQuery -``` - -LDBC performs type-safe query construction by passing table definitions to TableQuery. - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) - -val userQuery = TableQuery[User](table) -``` - -## SELECT - -A type-safe way to construct a SELECT statement is to use the `select` method provided by TableQuery, which is implemented in LDBC to mimic a plain query, making query construction intuitive. LDBC is also designed so that you can see at a glance what kind of query is being constructed. - -To construct a SELECT statement that retrieves only specific columns, simply specify the columns you want to retrieve in the `select` method. - -```scala 3 -val select = userQuery.select(_.id) - -select.statement === "SELECT `id` FROM user" -``` - -To specify multiple columns, simply specify the columns you wish to retrieve using the `select` method and return a tuple of the specified columns. - -```scala 3 -val select = userQuery.select(user => (user.id, user.name)) - -select.statement === "SELECT `id`, `name` FROM user" -``` - -If you want to specify all columns, you can construct it by using the `selectAll` method provided by TableQuery. - -```scala 3 -val select = userQuery.selectAll - -select.statement === "SELECT `id`, `name`, `age` FROM user" -``` - -If you want to get the number of a specific column, you can construct it by using `count` on the specified column. - -```scala 3 -val select = userQuery.select(_.id.count) - -select.statement === "SELECT COUNT(id) FROM user" -``` - -### WHERE - -A type-safe way to set a Where condition in a query is to use the `where` method. - -```scala 3 -val select = userQuery.select(_.id).where(_.name === "Test") - -select.statement === "SELECT `id` FROM user WHERE name = ?" -``` - -The following is a list of conditions that can be used in the `where` method. - -| condition | statement | -|--------------------------------------|---------------------------------------| -| === | `column = ?` | -| >= | `column >= ?` | -| > | `column > ?` | -| <= | `column <= ?` | -| < | `column < ?` | -| <> | `column <> ?` | -| !== | `column != ?` | -| IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL") | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | -| <=> | `column <=> ?` | -| IN (value, value, ...) | `column IN (?, ?, ...)` | -| BETWEEN (start, end) | `column BETWEEN ? AND ?` | -| LIKE (value) | `column LIKE ?` | -| LIKE_ESCAPE (like, escape) | `column LIKE ? ESCAPE ?` | -| REGEXP (value) | `column REGEXP ?` | -| `<<` (value) | `column << ?` | -| `>>` (value) | `column >> ?` | -| DIV (cond, result) | `column DIV ? = ?` | -| MOD (cond, result) | `column MOD ? = ?` | -| ^ (value) | `column ^ ?` | -| ~ (value) | `~column = ?` | - -### GROUP BY/Having - -A type-safe way to set a Group By clause in a query is to use the `groupBy` method. - -Using `groupBy` allows you to group data based on the value of a column name you specify when retrieving data with `select`. - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3) - -select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age" -``` - -When grouping, the number of data that can be retrieved with `select` is the number of groups. So, when grouping, you can retrieve the values of the columns specified for grouping, or the results of aggregating the column values by group using the provided functions. - -The `having` allows you to set the conditions for retrieval with respect to data grouped and retrieved by `groupBy`. - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3).having(_._3 > 20) - -select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age HAVING age > ?" -``` - -### ORDER BY - -A type-safe way to set an ORDER BY clause in a query is to use the `orderBy` method. - -Using `orderBy` allows you to get the results sorted by the values of the columns you specify when retrieving data with `select`. - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age) - -select.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age" -``` - -If you want to specify ascending/descending order, simply call `asc`/`desc` for the columns, respectively. - -```scala 3 -val desc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.desc) - -desc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age DESC" - -val asc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.asc) - -asc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age ASC" -``` - -### LIMIT/OFFSET - -A type-safe way to set the LIMIT and OFFSET clauses in a query is to use the `limit`/`offset` methods. - -The `limit` can be set to the maximum number of rows of data to retrieve when `select` is executed, and the `offset` can be set to the number of rows of data to retrieve. - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).limit(100).offset(50) - -select.statement === "SELECT `id`, `name`, `age` FROM user LIMIT ? OFFSET ?" -``` - -## JOIN/LEFT JOIN/RIGHT JOIN - -A type-safe way to set a Join on a query is to use the `join`/`leftJoin`/`rightJoin` methods. - -The following definition is used as a sample for Join. - -```scala 3 -case class Country(code: String, name: String) -object Country: - val table = Table[Country]("country")( - column("code", CHAR(3), PRIMARY_KEY), - column("name", VARCHAR(255)) - ) - -case class City(id: Long, name: String, countryCode: String) -object City: - val table = Table[City]("city")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("country_code", CHAR(3)) - ) - -case class CountryLanguage( - countryCode: String, - language: String -) -object CountryLanguage: - val table: Table[CountryLanguage] = Table[CountryLanguage]("country_language")( - column("country_code", CHAR(3)), - column("language", CHAR(30)) - ) - -val countryQuery = TableQuery[Country](Country.table) -val cityQuery = TableQuery[City](City.table) -val countryLanguageQuery = TableQuery[CountryLanguage](CountryLanguage.table) -``` - -If you want to do a simple Join first, use `join`. -The first argument of `join` is the table to be joined, and the second argument is a function that compares the source table with the columns of the table to be joined. This corresponds to the ON clause in Join. - -After the join, the `select` will specify columns from the two tables. - -```scala 3 -val join = countryQuery.join(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country JOIN city ON country.code = city.country_code" -``` - -Next, if you want to perform a Left Join, which is a left outer join, use `leftJoin`. -The implementation itself is the same as for a simple Join, only `join` is changed to `leftJoin`. - -```scala 3 -val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city ON country.code = city.country_code" -``` - -The difference from a simple Join is that when using `leftJoin`, the records retrieved from the table to be joined may be NULL. - -Therefore, in LDBC, all records in the column retrieved from the table passed to `leftJoin` will be of type Option. - -```scala 3 -val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (String, Option[String]) -``` - -Next, if you want to perform a Right Join, which is a right outer join, use `rightJoin`. -The implementation itself is the same as that of simple Join, only `join` is changed to `rightJoin`. - -```scala 3 -val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN city ON country.code = city.country_code" -``` - -The difference from a simple Join is that when using `rightJoin`, the records retrieved from the join source table may be NULL. - -Therefore, in LDBC, all records of a column retrieved from a join source table using `rightJoin` are of type Option. - -```scala 3 -val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (Option[String], String) -``` - -If multiple joins are desired, this can be accomplished by calling any Join method in the method chain. - -```scala 3 -val join = - (countryQuery join cityQuery)((country, city) => country.code === city.countryCode) - .rightJoin(countryLanguageQuery)((_, city, countryLanguage) => city.countryCode === countryLanguage.countryCode) - .select((country, city, countryLanguage) => (country.name, city.name, countryLanguage.language)) // (Option[String], Option[String], String)] - -join.statement = - """ - |SELECT - | country.`name`, - | city.`name`, - | country_language.`language` - |FROM country - |JOIN city ON country.code = city.country_code - |RIGHT JOIN country_language ON city.country_code = country_language.country_code - |""".stripMargin -``` - -Note that a `rightJoin` join with multiple joins will result in NULL-acceptable access to all records retrieved from the previously joined table, regardless of what the previous join was. - -## Custom Data Type - -In the previous section, we used the `mapping` method of DataType to map custom types to DataType in order to use user-specific or unsupported types. ([reference](/ldbc/en/02-Custom-Data-Type.html)) - -LDBC separates the table definition from the process of connecting to the database. -Therefore, if you want to retrieve data from the database and convert it to a user-specific or unsupported type, you must link the method of retrieving data from the ResultSet to the user-specific or unsupported type. - -For example, if you want to map a user-defined Enum to a string type - -```scala 3 -enum Custom: - case ... - -given ResultSetReader[IO, Custom] = - ResultSetReader.mapping[IO, str, Custom](str => Custom.valueOf(str)) -``` - -※ This process may be integrated with DataType mapping in a future version. - -## INSERT - -A type-safe way to construct an INSERT statement is to use the following methods provided by TableQuery. - -- insert -- insertInto -- += -- ++= - -**insert** - -The `insert` method is passed a tuple of data to insert. The tuples must have the same number and type of properties as the model. Also, the order of the inserted data must be in the same order as the model properties and table columns. - -```scala 3 -val insert = userQuery.insert((1L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" -``` - -If you want to insert multiple data, you can construct it by passing multiple tuples to the `insert` method. - -```scala 3 -val insert = userQuery.insert((1L, "name", None), (2L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -**insertInto** - -The `insert` method inserts data into all columns the table has, but if you want to insert data only into specific columns, use the `insertInto` method. - -This can be used, for example, to exclude data insertion into columns with AutoIncrement or Default values. - -```scala 3 -val insert = userQuery.insertInto(user => (user.name, user.age)).values(("name", None)) - -insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?)" -``` - -If you want to insert multiple data, you can construct it by passing an array of tuples to `values`. - -```scala 3 -val insert = userQuery.insertInto(user => (user.name, user.age)).values(List(("name", None), ("name", Some(20)))) - -insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?), (?, ?)" -``` - -**+=** - -The `+=` method can be used to construct an INSERT statement using a model. Note that when using a model, data is inserted into all columns. - -```scala 3 -val insert = userQuery += User(1L, "name", None) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" -``` - -**++=** - -Use the `++=` method if you want to insert multiple data using the model. - -```scala 3 -val insert = userQuery ++= List(User(1L, "name", None), User(2L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -### ON DUPLICATE KEY UPDATE - -Inserting a row with an ON DUPLICATE KEY UPDATE clause will cause an UPDATE of the old row if the UNIQUE index or PRIMARY KEY has duplicate values. - -There are two ways to achieve this in LDBC: using `insertOrUpdate{s}` or using `onDuplicateKeyUpdate` for `Insert`. - -```scala 3 -val insert = userQuery.insertOrUpdate((1L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `id` = new_user.`id`, `name` = new_user.`name`, `age` = new_user.`age`" -``` - -Note that if you use `insertOrUpdate{s}`, all columns will be updated. If you have duplicate values and wish to update only certain columns, use `onDuplicateKeyUpdate` to specify only the columns you wish to update. - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).onDuplicateKeyUpdate(v => (v.name, v.age)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `age` = new_user.`age`" -``` - -## UPDATE - -A type-safe way to construct an UPDATE statement is to use the `update` method provided by TableQuery. - -The first argument of the `update` method is the name of the model property, not the column name of the table, and the second argument is the value to be updated. The type of the value passed as the second argument must be the same as the type of the property specified in the first argument. - -```scala 3 -val update = userQuery.update("name", "update name") - -update.statement === "UPDATE user SET name = ?" -``` - -If a property name that does not exist is specified as the first argument, a compile error occurs. - -```scala 3 -val update = userQuery.update("hoge", "update name") // Compile error -``` - -If you want to update multiple columns, use the `set` method. - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20)) - -update.statement === "UPDATE user SET name = ?, age = ?" -``` - -You can also prevent the `set` method from generating queries based on conditions. - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20), false) - -update.statement === "UPDATE user SET name = ?" -``` - -You can also use a model to construct the UPDATE statement. Note that if you use a model, all columns will be updated. - -```scala 3 -val update = userQuery.update(User(1L, "update name", None)) - -update.statement === "UPDATE user SET id = ?, name = ?, age = ?" -``` - -### WHERE - -The `where` method can also be used to set a where condition on the update statement. - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20)).where(_.id === 1) - -update.statement === "UPDATE user SET name = ?, age = ? WHERE id = ?" -``` - -See [where item](/ldbc/en/03-Type-safe-Query-Builder.html#where) in the Insert statement for conditions that can be used in the `where` method. - -## DELETE - -A type-safe way to construct a DELETE statement is to use the `delete` method provided by TableQuery. - -```scala 3 -val delete = userQuery.delete - -delete.statement === "DELETE FROM user" -``` - -### WHERE - -The `where` method can also be used to set a Where condition on a delete statement. - -```scala 3 -val delete = userQuery.delete.where(_.id === 1) - -delete.statement === "DELETE FROM user WHERE id = ?" -``` - -See [where item](/ldbc/en/03-Type-safe-Query-Builder.html#where) in the Insert statement for conditions that can be used in the `where` method. - -## DDL - -A type-safe way to construct DDL is to use the following methods provided by TableQuery. - -- createTable -- dropTable -- truncateTable - -If you are using spec2, you can run DDL before and after the test as follows. - -```scala 3 -import cats.effect.IO -import cats.effect.unsafe.implicits.global - -import org.specs2.mutable.Specification -import org.specs2.specification.core.Fragments -import org.specs2.specification.BeforeAfterEach - -object Test extends Specification, BeforeAfterEach: - - override def before: Fragments = - step((tableQuery.createTable.update.autoCommit(dataSource) >> IO.println("Complete create table")).unsafeRunSync()) - - override def after: Fragments = - step((tableQuery.dropTable.update.autoCommit(dataSource) >> IO.println("Complete drop table")).unsafeRunSync()) -``` diff --git a/docs/src/main/mdoc/en/04-Database-Connection.md b/docs/src/main/mdoc/en/04-Database-Connection.md deleted file mode 100644 index 709a0c4d3..000000000 --- a/docs/src/main/mdoc/en/04-Database-Connection.md +++ /dev/null @@ -1,410 +0,0 @@ -# Database Connection - -This chapter describes how to use queries built with LDBC to process connections to databases. - -The following dependencies must be set up for the project - -@@@ vars -```scala -libraryDependencies ++= Seq( - "$org$" %% "ldbc-dsl" % "$version$", - "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" -) -``` -@@@ - -If you have not yet read about how to build queries with LDBC, we recommend that you read the chapter [Building Type-Safe Queries](/ldbc/en/03-Type-safe-Query-Builder.html) first. - -The following code example assumes the following import - -```scala 3 -import com.mysql.cj.jdbc.MysqlDataSource - -import cats.effect.IO -// This is just for testing. Consider using cats.effect.IOApp instead of calling -// unsafe methods directly. -import cats.effect.unsafe.implicits.global - -import ldbc.sql.* -import ldbc.dsl.io.* -import ldbc.dsl.logging.ConsoleLogHandler -import ldbc.query.builder.TableQuery -``` - -Table definitions use the following - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) - -val userQuery = TableQuery[User](table) -``` - -## Using DataSource - -LDBC uses JDBC's DataSource for database connections, and since LDBC does not provide an implementation for building this DataSource, it is necessary to use a library such as mysql or HikariCP. In this example, we will use mysqlDataSource to build the DataSource. - -```scala 3 -private val dataSource = new MysqlDataSource() -dataSource.setServerName("127.0.0.1") -dataSource.setPortNumber(3306) -dataSource.setDatabaseName("database name") -dataSource.setUser("user name") -dataSource.setPassword("password") -``` - -## Log - -LDBC can export execution and error logs of Database connections in any format using any logging library. - -A logger using Cats Effect's Console is provided as standard, which can be used during development. - -```scala 3 -given LogHandler[IO] = ConsoleLogHandler[IO] -``` - -### Customize - -Use `ldbc.dsl.logging.LogHandler` to customize logs using any logging library. - -The following is the standard implementation of logging: LDBC generates the following three types of events on database connections - -- Success: Successful processing -- ProcessingFailure: Error in processing after data acquisition or before database connection -- ExecFailure: Error processing connection to database - -Pattern matching is used to sort out what logs to write for each event. - -```scala 3 -def consoleLogger[F[_]: Console: Sync]: LogHandler[F] = - case LogEvent.Success(sql, args) => - Console[F].println( - s"""Successful Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) - case LogEvent.ProcessingFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed ResultSet Processing: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) - case LogEvent.ExecFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) -``` - -## Query - -Constructing a `select` statement allows the use of the `toList`/`headOption`/`unsafe` methods. These methods are used to determine the format of the data to be retrieved. If you do not specify any particular type, the column type specified in the `select` method will be returned as a Tuple. - -### toList - -The `toList` method is used to retrieve a list of data as a result of executing a query. If you use the `toList` method to process the database and get zero data, an empty array will be returned. - -```scala 3 -val query1 = userQuery.selectAll.toList // List[(Long, String, Option[Int])] -``` - -Specifying a model in the `toList` method allows the data after acquisition to be converted to the specified model. - -```scala 3 -val query = userQuery.selectAll.toList[User] // User -``` - -The model type specified in the `toList` method must match the Tuple type specified in the `select` method or be type-convertible from the Tuple type to the specified model. - -```scala 3 -val query1 = userQuery.select(user => (user.name, user.age)).toList[User] // Compile error - -case class Test(name: String, age: Option[Int]) -val query2 = userQuery.select(user => (user.name, user.age)).toList[Test] // Test -``` - -### headOption - -If you want to get the first data as an Optional result of the query, use the `headOption` method. If the result of database processing using the `headOption` method is zero, none is returned. - -Note that if you use the `headOption` method, only the first data will be returned, even if you execute a query that retrieves multiple data. - -```scala 3 -val query1 = userQuery.selectAll.headOption // Option[(Long, String, Option[Int])] -val query2 = userQuery.selectAll.headOption[User] // Option[User] -``` - -### unsafe - -When using the `unsafe` method, it is the same as the `headOption` method in that it returns only the first case of the retrieved data, but the data is returned as is, not as Optional. If the number of data returned is zero, an exception will be raised and appropriate exception handling is required. - -It is named `unsafe` because it is likely to raise an exception at runtime. - -```scala 3 -val query1 = userQuery.selectAll.unsafe // (Long, String, Option[Int]) -val query2 = userQuery.selectAll.unsafe[User] // User -``` - -## Update - -Constructing an `insert/update/delete` statement allows you to use the `update` method. The `update` method returns the number of write operations to the database. - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).update // Int -val update = userQuery.update("name", "update name").update // Int -val delete = userQuery.delete.update // Int -``` - -In the case of an `insert` statement, you may want the values generated by AutoIncrement to be returned when inserting data. In this case, use the `returning` method instead of the `update` method to specify the columns to be returned. - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).returning("id") // Long -``` - -The value specified in the `returning` method must be the name of a property that the model has. Also, if the specified property does not have the AutoIncrement attribute set on the table definition, an error will occur. - -In MySQL, the only value that can be returned when inserting data is the AutoIncrement column, so the same specification applies to LDBC. - -## Perform database operations - -Before making a database connection, commit timing, read/write-only, and other settings must be made. - -### Read Only - -The `readOnly` method can be used to make the processing of a query to be executed read-only. The `readOnly` method can also be used with `insert/update/delete` statements, but it will result in an error at runtime because of the write operation. - -```scala 3 -val read = userQuery.selectAll.toList.readOnly(dataSource) -``` - -### Auto Commit - -The `autoCommit` method can be used to set the query processing to commit at each query execution. - -```scala 3 -val read = userQuery.insert((1L, "name", None)).update.autoCommit(dataSource) -``` - -### Transaction - -The `transaction` method can be used to combine multiple database connection operations into a single transaction. - -The return value of the `toList/headOption/unsafe/returning/update` method is of type `Kleisli[F, Connection[F], T]`. Therefore, you can use map or flatMap to combine the process into one. - -By using the `transaction` method on a single `Kleisli[F, Connection[F], T]`, all database connection operations performed within will be combined into a single transaction. - -```scala 3 -(for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction(dataSource) -``` - -## Database Action - -There is also a way to perform database processing using `Database` with connection information to the database. - -There are two ways to construct a `Database`: using the DriverManager or generating one from a DataSource. The following is an example of constructing a `Database` with connection information to a database using a MySQL driver. - -```scala 3 -val db = Database.fromMySQLDriver[IO]("database name", "host", "port number", "user name", "password") -``` - -The advantages of using `Database` to perform database processing are as follows - -- Simplifies DataSource construction (when using DriverManager) -- Eliminates the need to pass a DataSource for each query - -The method using `Database` is merely a simplified method of passing a DataSource, so there is no difference in execution results between the two. -The only difference is whether the processes are combined using `flatMap` or other methods and executed in a method chain, or whether the combined processes are executed using `Database`. Therefore, the user can choose the execution method of his/her choice. - -**Read Only** - -```scala 3 -val user: Option[User] = db.readOnly(userQuery.selectAll.headOption[User]).unsafeRunSync() -``` - -**Auto Commit** - -```scala 3 -val result = db.autoCommit(userQuery.insert((1L, "name", None)).update).unsafeRunSync() -``` - -**Transaction** - -```scala 3 -db.transaction(for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).unsafeRunSync() -``` - -### Database model - -In LDBC, the `Database` model is also used for purposes other than holding database connection information. Another use is for SchemaSPY documentation generation, see [here](/ldbc/ja/06-Generating-SchemaSPY-Documentation.html) for information on SchemaSPY document generation. - -If you have already generated a `Database` model for another use, you can use that model to build a `Database` with database connection information. - -```scala 3 -import ldbc.dsl.io.* - -val database: Database = ??? - -val db = database.fromDriverManager() -// or -val db = database.fromDriverManager("user name", "password") -``` - -### Use in method chain - -The `Database` model can also be used in place of `DataSource` in `TableQuery` methods. - -```scala 3 -val read = userQuery.selectAll.toList.readOnly(db) -val commit = userQuery.insert((1L, "name", None)).update.autoCommit(db) -val transaction = (for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction(db) -``` - -## Using a HikariCP Connection Pool - -`ldbc-hikari` provides a builder to build HikariConfig and HikariDataSource for building HikariCP connection pools. - -@@@ vars -```scala -libraryDependencies ++= Seq( - "$org$" %% "ldbc-hikari" % "$version$", -) -``` -@@@ - -`HikariConfigBuilder` is a builder to build `HikariConfig` of HikariCP as the name suggests. - -```scala 3 -val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.default.build() -``` - -The `HikariConfigBuilder` has a `default` and a `from` method. When `default` is used, the `HikariConfig` is constructed by retrieving the target values from the Config based on the LDBC specified path. - -```text -ldbc.hikari { - jdbc_url = ... - username = ... - password = ... -} -``` - -If you want to specify a user-specific path, you must use the `from` method and pass the path you want to retrieve as an argument. - -```scala 3 -val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.from("custom.path").build() - -// custom.path { -// jdbc_url = ... -// username = ... -// password = ... -// } -``` - -Please refer to [official](https://github.com/brettwooldridge/HikariCP) for details on what can be set in HikariCP. - -The following is a list of keys that can be set for Config. - -| Key name | Description | Type | -|-----------------------------|---------------------------------------------------------------------------------------------------------------------------|----------| -| catalog | Default catalog name to be set when connecting | String | -| connection_timeout | Maximum number of milliseconds the client will wait for a connection from the pool | Duration | -| idle_timeout | Maximum time (in milliseconds) that a connection is allowed to be idle in the pool | Duration | -| leak_detection_threshold | Time a connection is out of the pool before a message indicating a possible connection leak is logged | Duration | -| maximum_pool_size | Maximum size allowed by the pool, including both idle and in-use connections | Int | -| max_lifetime | Maximum lifetime of connections in the pool | Duration | -| minimum_idle | Minimum number of idle connections that HikariCP will try to keep in the pool, including both idle and in-use connections | Int | -| pool_name | Connection pool name | String | -| allow_pool_suspension | Whether to allow pool suspend | Boolean | -| auto_commit | Default autocommit behavior for connections in the pool | Boolean | -| connection_init_sql | SQL string to be executed when a new connection is created, before it is added to the pool | String | -| connection_test_query | SQL query to execute to test the validity of the connection | String | -| data_source_classname | Fully qualified class name of the JDBC DataSource to be used to create Connections | String | -| initialization_fail_timeout | Pool initialization failure timeout | Duration | -| isolate_internal_queries | Whether internal pool queries (mainly validity checks) are separated in their own transaction by `Connection.rollback()`. | Boolean | -| jdbc_url | JDBC URL | String | -| readonly | Whether connections to be added to the pool should be set as read-only connections | Boolean | -| register_mbeans | Whether HikariCP self-registers HikariConfigMXBean and HikariPoolMXBean in JMX | Boolean | -| schema | Default schema name to set when connecting | String | -| username | Default username used for calls to `DataSource.getConnection(username,password)` | String | -| password | Default password used for calling `DataSource.getConnection(username,password)` | String | -| driver_class_name | Driver class name to be used | String | -| transaction_isolation | Default transaction isolation level | String | - -The `HikariDataSourceBuilder` allows you to build a `HikariDataSource` for HikariCP. - -The `HikariDataSource` built by the builder is managed as a `Resource` since the connection pool is a lifetime managed object and needs to be shut down cleanly. - -```scala 3 -val dataSource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() -``` - -The `HikariDataSource` built via `buildDataSource` uses `HikariConfig`, which is built internally by retrieving settings from Config based on the LDBC specified path. -This is equivalent to `HikariConfig` generated via `default` in `HikariConfigBuilder`. - -If you want to use a user-specified `HikariConfig`, you can use `buildFromConfig` to build a `HikariDataSource`. - -```scala 3 -val hikariConfig = ??? -val dataSource = HikariDataSourceBuilder.default[IO].buildFromConfig(hikariConfig) -``` - -A `HikariDataSource` built with `HikariDataSourceBuilder` is usually executed using IOApp. - -```scala 3 -object HikariApp extends IOApp: - - val dataSourceResource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() - - def run(args: List[String]): IO[ExitCode] = - dataSourceResource.use { dataSource => - ... - } -``` - -### HikariDatabase - -There is also a way to build a `Database` with HikariCP connection information. - -The `HikariDatabase` is managed as a `Resource` like the `HikariDataSource`. -Therefore, it is usually executed using IOApp. - -```scala 3 -object HikariApp extends IOApp: - - val hikariConfig = ??? - val databaseResource: Resource[F, Database[F]] = HikariDatabase.fromHikariConfig[IO](hikariConfig) - - def run(args: List[String]): IO[ExitCode] = - databaseResource.use { database => - for - result <- database.readOnly(...) - yield ExitCode.Success - } -``` diff --git a/docs/src/main/mdoc/en/05-Plain-SQL-Queries.md b/docs/src/main/mdoc/en/05-Plain-SQL-Queries.md deleted file mode 100644 index e8ee83d13..000000000 --- a/docs/src/main/mdoc/en/05-Plain-SQL-Queries.md +++ /dev/null @@ -1,34 +0,0 @@ -# Plain SQL Queries - -Sometimes you may need to write your own SQL code for operations that are not well supported at a higher level of abstraction; instead of going back to the lower layers of JDBC, you can use LDBC's Plain SQL queries in the Scala-based API. -This chapter describes how to use Plain SQL queries in LDBC to process connections to databases in such cases. - -See the previous chapter on [Database Connections](/ldbc/en/04-Database-Connection.html) for project dependencies and the use and logging of DataSource. - -## Plain SQL - -LDBC uses sql string interpolation with literal SQL strings to construct plain queries as follows - -Variables and expressions injected into the query are converted to bind variables in the resulting query string. Since they are not inserted directly into the query string, there is no risk of SQL injection attacks. - -```scala 3 -val select = sql"SELECT id, name, age FROM user WHERE id = $id" // SELECT id, name, age FROM user WHERE id = ? -val insert = sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)" // INSERT INTO user (id, name, age) VALUES(?, ?, ?) -val update = sql"UPDATE user SET id = $id, name = $name, age = $age" // UPDATE user SET id = ?, name = ?, age = ? -val delete = sql"DELETE FROM user WHERE id = $id" // DELETE FROM user WHERE id = ? -``` - -Plain SQL queries simply construct SQL statements at runtime. While this provides a safe and easy way to construct complex statements, it is merely an embedded string. Any syntax errors in the statement or type mismatch between the database and the Scala code cannot be detected at compile time. - -Please refer to the [Query](/ldbc/en/04-Database-Connection.html#Query) item in the previous section "Database Connection" for information on setting the return type of the query result and the connection method. -It is built and works the same way as a query built using a table definition. - -Plain queries and type-safe queries are constructed differently, but the implementation is the same, including the subsequent connection methods. Therefore, it is possible to combine the two and execute the query. - -```scala 3 -(for - result1 <- sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)".update - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction -``` diff --git a/docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md b/docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md deleted file mode 100644 index 1d6da60cd..000000000 --- a/docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md +++ /dev/null @@ -1,76 +0,0 @@ -# SchemaSPY Document Generation - -This chapter describes how to use table definitions built in LDBC to create SchemaSPY documents. - -The following dependencies must be set up for the project - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-schemaspy" % "$version$" -``` -@@@ - -If you have not yet read how to define tables in LDBC, we recommend that you read the chapter [Table Definitions](/ldbc/en/01-Table-Definitions.html) first. - -The following code example assumes the following import - -```scala 3 -import ldbc.core.* -import ldbc.schemaspy.SchemaSpyGenerator -``` - -## Generated from table definitions - -SchemaSPY connects to the database to obtain Meta information and table structures, and generates documents based on this information. LDBC, on the other hand, does not connect to the database, but generates SchemaSPY documents using the table structures constructed by LDBC. -Some items deviate from the documentation generated using SchemaSPY simply because it does not make a connection to the database. For example, information such as the number of records currently stored in a table cannot be displayed. - -Database information is required to generate documents, and LDBC has a trait for representing database information. - -A sample of database information built using `ldbc.core.Database` is shown below. - -```scala 3 -case class SampleLdbcDatabase( - schemaMeta: Option[String] = None, - catalog: Option[String] = Some("def"), - host: String = "127.0.0.1", - port: Int = 3306 -) extends Database: - - override val databaseType: Database.Type = Database.Type.MySQL - - override val name: String = "sample_ldbc" - - override val schema: String = "sample_ldbc" - - override val character: Option[Character] = None - - override val collate: Option[Collate] = None - - override val tables = Set( - ... // Enumerate table structures built with LDBC - ) -``` - -Use `SchemaSpyGenerator` to generate SchemaSPY documents. Pass the generated database definition to the `default` method and call `generate` to generate SchemaSPY files at the file location specified in the second argument. - -```scala 3 -@main -def run(): Unit = - val file = java.io.File("document") - SchemaSpyGenerator.default(SampleLdbcDatabase(), file).generate() -``` - -Open the generated file `index.html` to see the SchemaSPY documentation. - -## Generated from database connection - -SchemaSpyGenerator also has a `connect` method. This method connects to the database and generates documents in the same way as the standard SchemaSpy generator. - -```scala 3 -@main -def run(): Unit = - val file = java.io.File("document") - SchemaSpyGenerator.connect(SampleLdbcDatabase(), "user name", "password" file).generate() -``` - -The process of making database connections is done in a Java-written implementation inside SchemaSpy. Note that threads are not managed by the Effect system. diff --git a/docs/src/main/mdoc/en/07-Schema-Code-Generation.md b/docs/src/main/mdoc/en/07-Schema-Code-Generation.md deleted file mode 100644 index dc77a0ec3..000000000 --- a/docs/src/main/mdoc/en/07-Schema-Code-Generation.md +++ /dev/null @@ -1,202 +0,0 @@ -# Schema Code Generation - -This chapter describes how to automatically generate LDBC table definitions from SQL files. - -The following dependencies must be set up for the project - -@@@ vars -```scala 3 -addSbtPlugin("$org$" % "ldbc-plugin" % "$version$") -``` -@@@ - -## Generation - -Enable the plugin for the project. - -```sbt -lazy val root = (project in file(".")) - .enablePlugins(Ldbc) -``` - -Specify the SQL file to be analyzed as an array. - -```sbt -Compile / parseFiles := List(baseDirectory.value / "test.sql") -``` - -**List of keys that can be set by enabling the plugin** - -| Key | Details | -|--------------------|----------------------------------------------------------------------| -| parseFiles | List of SQL files to be analyzed | -| parseDirectories | Specify SQL files to be parsed by directory | -| excludeFiles | List of file names to exclude from analysis | -| customYamlFiles | List of yaml files for customizing Scala types and column data types | -| classNameFormat | Value specifying the format of the class name | -| propertyNameFormat | Value specifying the format of the property name in the Scala model | -| ldbcPackage | Value specifying the package name of the generated file | - -The SQL file to be parsed must always begin with a database Create or Use statement, and LDBC parses the file one file at a time, generating table definitions and storing the list of tables in the database model. -This is because it is necessary to tell which database the table belongs to. - -```mysql -CREATE DATABASE `location`; - -USE `location`; - -DROP TABLE IF EXISTS `country`; -CREATE TABLE country ( - `id` BIGINT AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(255) NOT NULL, - `code` INT NOT NULL -); -``` - -The SQL file to be analyzed should contain only Create/Use statements for the database or Create/Drop statements for table definitions. - -## Generation Code - -When the sbt project is started and compiled, model classes generated based on the SQL file to be analyzed and table definitions are generated under the target of the sbt project. - -```shell -sbt compile -``` - -The code generated from the above SQL file will look like this. - -```scala 3 -package ldbc.generated.location - -import ldbc.core.* - -case class Country( - id: Long, - name: String, - code: Int -) - -object Country: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -If the SQL file has been modified or the cache has been removed by running the clean command, Compile will generate the code again. If the SQL file has been modified or the cache has been removed by executing the clean command, the code will be generated again by executing Compile. -If you want to generate code again without using the cache, execute the command `generateBySchema`. This command will always generate code without using the cache. - -```shell -sbt generateBySchema -``` - -## Customize - -There may be times when you want to convert the type of code generated from an SQL file to something else. This can be done by passing `customYamlFiles` with the yml files to be customized. - -```sbt -Compile / customYamlFiles := List( - baseDirectory.value / "custom.yml" -) -``` - -The format of the yml file should be as follows - -```yaml -database: - name: '{Database Name}' - tables: - - name: '{table name}' - columns: # Optional - - name: '{column name}' - type: '{Scala type you want to change}' - class: # Optional - extends: - - '{Package paths such as trait that you want model classes to inherit}' // package.trait.name - object: # Optional - extends: - - '{The package path, such as trait, that you want the object to inherit.}' - - name: '{table name}' - ... -``` - -The `database` must be the name of the database listed in the SQL file to be analyzed. The table name must be the name of a table belonging to the database listed in the SQL file to be analyzed. - -In the `columns` field, enter the name of the column to be retyped and the Scala type to be changed as a string. You can set multiple values for `columns`, but the column name listed in name must be in the target table. -Also, the Scala type to be converted must be one that is supported by the column's Data type. If you want to specify an unsupported type, you must pass a trait, abstract class, etc. that is configured to do implicit type conversion for `object`. - -See [here](/ldbc/en/01-Table-Definitions.html) for types supported by the Data type and [here](/ldbc/en/02-Custom-Data-Type.html). - -To convert an Int type to the user's own type, CountryCode, implement the following `CustomMapping`trait. - -```scala 3 -trait CountryCode: - val code: Int -object Japan extends CountryCode: - override val code: Int = 1 - -trait CustomMapping: // Any name - given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode] -``` - -Set the `CustomMapping`trait that you have implemented in the yml file for customization, and convert the target column type to CountryCode. - -```yaml -database: - name: 'location' - tables: - - name: 'country' - columns: - - name: 'code' - type: 'Country.CountryCode' // CustomMapping is mixed in with the Country object so that it can be retrieved from there. - object: - extends: - - '{package.name.}CustomMapping' -``` - -The code generated by the above configuration will be as follows, allowing users to generate model and table definitions with their own types. - -```scala 3 -case class Country( - id: Long, - name: String, - code: Country.CountryCode -) - -object Country extends /*{package.name.}*/CustomMapping: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -The database model is also automatically generated from SQL files. - -```scala 3 -package ldbc.generated.location - -import ldbc.core.* - -case class LocationDatabase( - schemaMeta: Option[String] = None, - catalog: Option[String] = Some("def"), - host: String = "127.0.0.1", - port: Int = 3306 -) extends Database: - - override val databaseType: Database.Type = Database.Type.MySQL - - override val name: String = "location" - - override val schema: String = "location" - - override val character: Option[Character] = None - - override val collate: Option[Collate] = None - - override val tables = Set( - Country.table - ) -``` diff --git a/docs/src/main/mdoc/en/08-Perdormance.md b/docs/src/main/mdoc/en/08-Perdormance.md deleted file mode 100644 index c7b34a8f8..000000000 --- a/docs/src/main/mdoc/en/08-Perdormance.md +++ /dev/null @@ -1,39 +0,0 @@ -# Performance - -## Compile-time Overhead - -Compilation time for table definitions increases with the number of columns - -

Create compile time

- - -Compile time for query construction increases with the number of columns to select - -

Create query compile time

- - -## Runtime Overhead - -Since ldbc uses Tuple internally, it is much slower than pure class definition. - -

Create runtime

- - -ldbc is much slower than the others with table definitions. - -

Create query runtime

- - -## Query execution Overhead - -Throughput of select query execution decreases as the number of records to retrieve increases. - -

Select Throughput

- - -Throughput of insert query execution decreases as the number of records to insert increases. - -※ Not accurate because the query performed is not an exact match. - -

Insert Throughput

- diff --git a/docs/src/main/mdoc/en/09-Connector.md b/docs/src/main/mdoc/en/09-Connector.md deleted file mode 100644 index bdae5719b..000000000 --- a/docs/src/main/mdoc/en/09-Connector.md +++ /dev/null @@ -1,848 +0,0 @@ -# Connector - -This chapter describes database connections using LDBC's own MySQL connector. - -To make a connection to a MySQL database in Scala, you need to use JDBC, which is a standard Java API that can also be used in Scala. -JDBC is implemented in Java and can only work in a JVM environment, even when used in Scala. - -The recent environment surrounding Scala has seen a lot of development of plug-ins to work with JS, Native, and other environments. -Scala continues to evolve from a language that runs only in the JVM, where Java assets can be used, to one that can run in a multi-platform environment. - -However, JDBC is a standard Java API and does not support operation in Scala's multiplatform environment. - -Therefore, even if you create an application in Scala that can run on JS, Native, etc., you will not be able to connect to databases such as MySQL because you cannot use JDBC. - -Typelevel Project has a Scala library for [PostgreSQL](https://www.postgresql.org/) called [Skunk](https://github.com/typelevel/skunk). -This project does not use JDBC and uses only pure Scala to connect to PostgreSQL. Therefore, Skunk can be used to connect to PostgreSQL in any JVM, JS, or Native environment. - -The LDBC connector is a Skunk-inspired project that is being developed to enable connections to MySQL in any JVM, JS, or Native environment. - -※ This connector is currently an experimental feature. Therefore, please do not use it in a production environment. - -The LDBC connector is the lowest layer API. -We plan to use this connector to provide higher-layer APIs in the future. We also plan to make it compatible with existing higher-layer APIs. - -The following dependencies must be set up in your project in order to use it. - -**JVM** - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" -``` -@@@ - -**JS/Native** - -@@@ vars -```scala -libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" -``` -@@@ - -**Supported Versions** - -The current version supports the following versions of MySQL - -- MySQL 5.7.x -- MySQL 8.x - -The main support is for MySQL 8.x. MySQL 5.7.x is a sub-support. Therefore, be careful when working with MySQL 5.7.x. -We plan to discontinue support for MySQL 5.7.x in the future. - -## Connection - -Use `Connection` to make a connection to MySQL using the LDBC connector. - -In addition, `Connection` allows the use of `Otel4s` to collect telemetry data in order to allow observer-aware development. -Therefore, when using `Connection`, the `Tracer` of `Otel4s` must be set. - -It is recommended to use `Tracer.noop` during development or when telemetry data using traces is not needed. - -```scala -import cats.effect.IO -import org.typelevel.otel4s.trace.Tracer -import ldbc.connector.Connection - -given Tracer[IO] = Tracer.noop[IO] - -val connection = Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "root", -) -``` - -The following is a list of properties that can be set when constructing a `Connection`. - -| Property | Type | Use | -|-------------------------|--------------------|------------------------------------------------------------------------------------------------------------| -| host | String | Specify the host for the MySQL server | -| port | Int | Specify the port number of the MySQL server | -| user | String | Specify the user name to log in to the MySQL server | -| password | Option[String] | Specify the password of the user who will log in to the MySQL server | -| database | Option[String] | Specify the database name to be used after connecting to the MySQL server | -| debug | Boolean | Outputs a log of the process. Default is false. | -| ssl | SSL | Specifies whether SSL/TLS is used for notifications to and from the MySQL server. The default is SSL.None. | -| socketOptions | List[SocketOption] | Specifies socket options for TCP/UDP sockets. | -| readTimeout | Duration | Specifies the timeout before an attempt is made to connect to the MySQL server. Default is Duration.Inf. | -| allowPublicKeyRetrieval | Boolean | Specifies whether to use the RSA public key when authenticating with the MySQL server. Default is false. | - -Connection` uses `Resource` to manage resources. Therefore, when connection information is used, the `use` method is used to manage the resource. - -```scala -connection.use { conn => - // Write code -} -``` - -### Authentication - -Authentication in MySQL involves the client sending user information in a phase called LoginRequest when connecting to the MySQL server. The server then looks up the user in the `mysql.user` table to determine which authentication plugin to use. After the authentication plugin is determined, the server calls the plugin to initiate user authentication and sends the results to the client. In this way, authentication is pluggable (various types of plug-ins can be added and removed) in MySQL. - -Authentication plug-ins supported by MySQL are listed on the [official page](https://dev.mysql.com/doc/refman/8.0/ja/authentication-plugins.html). - -LDBC currently supports the following authentication plug-ins - -- Native pluggable authentication -- SHA-256 pluggable authentication -- Cache of SHA-2 pluggable certificates - -※ Native pluggable authentication and SHA-256 pluggable authentication are plugins that have been deprecated since MySQL 8.x. It is recommended that you use the SHA-2 pluggable authentication cache unless you have a good reason to do otherwise. - -There is no need to be aware of authentication plug-ins in the LDBC application code. Users simply create a user created with the authentication plugin they wish to use on the MySQL database and then attempt to connect to MySQL using that user in the LDBC application code. -LDBC will internally determine the authentication plugin and use the appropriate authentication plugin to connect to MySQL. - -## Execution - -The following tables are assumed to be used in the subsequent process. - -```sql -CREATE TABLE users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - age INT NULL -); -``` - -### Statement - -`Statement` is an API for executing SQL without dynamic parameters. - -※ Since `Statement` does not use dynamic parameters, there is a risk of SQL injection depending on its usage. Therefore, it is recommended to use `PreparedStatement` when dynamic parameters are used. - -Construct a `Statement` using the `createStatement` method of `Connection`. - -#### Read query - -Use the `executeQuery` method to execute read-only SQL. - -The values returned by the MySQL server as a result of executing the query are stored in a `ResultSet` and returned as the return value. - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeQuery("SELECT * FROM users") - yield - // Processing with ResultSet -} -``` - -#### Write Query - -Use the `executeUpdate` method to execute SQL to write. - -The value returned by the MySQL server as a result of executing the query is the number of rows affected. - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)") - yield -} -``` - -#### Get the value of AUTO_INCREMENT - -Use the `getGeneratedKeys` method to retrieve the AUTO_INCREMENT value after the query is executed using `Statement`. - -The value returned by the MySQL server as a result of executing the query will be the value generated for AUTO_INCREMENT as the return value. - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)", Statement.RETURN_GENERATED_KEYS) - gereatedKeys <- statement.getGeneratedKeys() - yield -} -``` - -### Client/Server PreparedStatement - -LDBC provides `PreparedStatement` divided into `Client PreparedStatement` and `Server PreparedStatement`. - -`Client PreparedStatement` is an API for constructing SQL on the application using dynamic parameters and sending it to the MySQL server. -Therefore, the method of sending queries to the MySQL server is the same as for `Statement`. - -This API is equivalent to JDBC's `PreparedStatement`. - -A `PreparedStatement` for building queries in a more secure MySQL server is provided in the `Server PreparedStatement`, so please use that. - -`Server PreparedStatement` is an API that prepares the query to be executed in advance in the MySQL server and executes it by setting parameters in the application. - -The `Server PreparedStatement` allows reuse of queries, since the query to be executed and the parameters are sent separately. - -When using `Server PreparedStatement`, the query is prepared in advance by the MySQL server. Although the MySQL server uses memory to store them, the queries can be reused, which improves performance. - -However, there is a risk of memory leaks because the pre-prepared query will continue to use memory until it is freed. - -If you use `Server PreparedStatement`, you must use the `close` method to properly release the query. - -#### Client PreparedStatement - -Construct a `Client PreparedStatement` using the `ClientPreparedStatement` method of `Connection`. - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### Server PreparedStatement - -Construct a `Server PreparedStatement` using the `Connection` `serverPreparedStatement` method. - -```scala -connection.use { conn => - for - statement <- conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### Read query - -Use the `executeQuery` method to execute read-only SQL. - -The values returned by the MySQL server as a result of executing the query are stored in a `ResultSet` and returned as the return value. - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - yield - // Processing with ResultSet -} -``` - -If you want to use dynamic parameters, use the `setXXX` method to set the parameters. -The `setXXX` method can also use the `Option` type. If `None` is passed, the parameter will be set to NULL. - -The `setXXX` method specifies the index of the parameter and the value of the parameter. - -```scala -statement.setLong(1, 1) -``` - -The following methods are supported in the current version - -| Method | Type | Note | -|---------------|:------------------------------------|----------------------------------------------------| -| setNull | | Set the parameter to NULL | -| setBoolean | Boolean/Option[Boolean] | | -| setByte | Byte/Option[Byte] | | -| setShort | Short/Option[Short] | | -| setInt | Int/Option[Int] | | -| setLong | Long/Option[Long] | | -| setBigInt | BigInt/Option[BigInt] | | -| setFloat | Float/Option[Float] | | -| setDouble | Double/Option[Double] | | -| setBigDecimal | BigDecimal/Option[BigDecimal] | | -| setString | String/Option[String] | | -| setBytes | Array[Byte]/Option[Array[Byte]] | | -| setDate | LocalDate/Option[LocalDate] | Directly handle `java.time` instead of `java.sql`. | -| setTime | LocalTime/Option[LocalTime] | Directly handle `java.time` instead of `java.sql`. | -| setTimestamp | LocalDateTime/Option[LocalDateTime] | Directly handle `java.time` instead of `java.sql`. | -| setYear | Year/Option[Year] | Directly handle `java.time` instead of `java.sql`. | - -#### Write Query - -Use the `executeUpdate` method to execute the SQL to be written. - -The value returned by the MySQL server as a result of executing the query is the number of rows affected. - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - yield result -} - -``` - -#### Get the value of AUTO_INCREMENT - -Use the `getGeneratedKeys` method to retrieve the value of AUTO_INCREMENT after executing the query. - -The value returned by the MySQL server as a result of executing the query will be the value generated for AUTO_INCREMENT as the return value. - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.executeUpdate() - getGeneratedKeys <- statement.getGeneratedKeys() - yield getGeneratedKeys -} -``` - -### ResultSet - -The `ResultSet` is an API for storing values returned by the MySQL server after query execution. - -There are two ways to retrieve records retrieved by executing SQL from a `ResultSet`: using the `next` and `getXXX` methods as in JDBC, or using LDBC's own `decode` method. - -#### next/getXXX - -The `next` method returns `true` if the next record exists, or `false` if the next record does not exist. - -The `getXXX` method is an API for retrieving values from a record. - -The `getXXX` method can be used either by specifying the index of the column to be retrieved or by specifying the column name. - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT `id`, `name`, `age` FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - records <- Monad[IO].whileM(result.next()) { - for - id <- result.getLong(1) - name <- result.getString("name") - age <- result.getInt(3) - yield (id, name, age) - } - yield records -} -``` - -#### decode - -The `decode` method is used to retrieve records from the `ResultSet` after they have been retrieved by executing SQL. - -The `decode` method is an API for converting values retrieved from `ResultSet` to Scala types. - -The type to be converted is specified using the `*:` operator depending on the number of columns to be retrieved. - -The example shows how to retrieve the id, name, and age columns of the users table, specifying the type of each column. - -```scala -result.decode(bigint *: varchar *: int.opt) -``` - -If you want to get a NULL-allowed column, use the `opt` method to convert it to the `Option` type. -If the record is NULL, it can be retrieved as None. - -The sequence of events from query execution to record retrieval is as follows - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - decodes <- result.decode(bigint *: varchar *: int.opt) - yield decodes -} -``` - -The records retrieved from a `ResultSet` will always be an array. -This is because a query in MySQL may always return multiple records. - -If you want to retrieve a single record, use the `head` or `headOption` method after the `decode` process. - -The following data types are supported in the current version - -| Codec | Data Type | Scala Type | -|-------------|-------------------|----------------| -| boolean | BOOLEAN | Boolean | -| tinyint | TINYINT | Byte | -| utinyint | unsigned TINYINT | Short | -| smallint | SMALLINT | Short | -| usmallint | unsigned SMALLINT | Int | -| int | INT | Int | -| uint | unsigned INT | Long | -| bigint | BIGINT | Long | -| ubigint | unsigned BIGINT | BigInt | -| float | FLOAT | Float | -| double | DOUBLE | Double | -| decimal | DECIMAL | BigDecimal | -| char | CHAR | String | -| varchar | VARCHAR | String | -| binary | BINARY | Array[Byte] | -| varbinary | VARBINARY | String | -| tinyblob | TINYBLOB | String | -| blob | BLOB | String | -| mediumblob | MEDIUMBLOB | String | -| longblob | LONGBLOB | String | -| tinytext | TINYTEXT | String | -| text | TEXT | String | -| mediumtext | MEDIUMTEXT | String | -| longtext | LONGTEXT | String | -| enum | ENUM | String | -| set | SET | List[String] | -| json | JSON | String | -| date | DATE | LocalDate | -| time | TIME | LocalTime | -| timetz | TIME | OffsetTime | -| datetime | DATETIME | LocalDateTime | -| timestamp | TIMESTAMP | LocalDateTime | -| timestamptz | TIMESTAMP | OffsetDateTime | -| year | YEAR | Year | - -※ Currently, it is designed to retrieve values by specifying the MySQL data type, but in the future it may be changed to a more concise Scala type to retrieve values. - -The following data types are not supported - -- GEOMETRY -- POINT -- LINESTRING -- POLYGON -- MULTIPOINT -- MULTILINESTRING -- MULTIPOLYGON -- GEOMETRYCOLLECTION - -## Transaction - -To execute a transaction using `Connection`, use the `setAutoCommit` method in combination with the `commit` and `rollback` methods. - -First, use the `setAutoCommit` method to disable transaction autocommit. - -```scala -conn.setAutoCommit(false) -``` - -Use the `commit` method to commit the transaction after some processing. - -```scala -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.commit() -yield -``` - -Or use the `rollback` method to roll back the transaction. - -```scala -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.rollback() -yield -``` - -If transaction autocommit is disabled using the `setAutoCommit` method, rollback will occur automatically when the connection's Resource is released. - -### Transaction isolation level - -LDBC allows for the setting of transaction isolation levels. - -The transaction isolation level is set using the `setTransactionIsolation` method. - -The following transaction isolation levels are supported in MySQL. - -- READ UNCOMMITTED -- READ COMMITTED -- REPEATABLE READ -- SERIALIZABLE - -See [official documentation](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html) for more information on transaction isolation levels in MySQL. - -```scala -import ldbc.connector.Connection.TransactionIsolationLevel - -conn.setTransactionIsolation(TransactionIsolationLevel.REPEATABLE_READ) -``` - -Use the `getTransactionIsolation` method to get the currently set transaction isolation level. - -```scala -for - isolationLevel <- conn.getTransactionIsolation() -yield -``` - -### Savepoint - -For more advanced transaction management, the "Savepoint feature" can be used. This allows you to mark a specific point during a database operation so that if something goes wrong, you can rewind the database state back to that point. This is especially useful for complex database operations or when you need to set a safe point in a long transaction. - -**Features:** - -- Flexible Transaction Management: Use Savepoint to create a "checkpoint" anywhere within a transaction. State can be returned to that point as needed. -- Error Recovery: Save time and increase efficiency by going back to the last safe Savepoint when an error occurs, rather than starting all over. -- Advanced Control: Multiple Savepoints can be configured for more precise transaction control. Developers can easily implement more complex logic and error handling. - -By taking advantage of this feature, your application will be able to achieve more robust and reliable database operations. - -**Savepoint Settings** - -To set a Savepoint, use the `setSavepoint` method. This method allows you to specify a name for the Savepoint. -If you do not specify a name for the Savepoint, the value generated by the UUID will be set as the default name. - -The `getSavepointName` method can be used to retrieve the name of the configured Savepoint. - -※ Since autocommit is enabled by default in MySQL, it is necessary to disable autocommit when using Savepoint. Otherwise, all operations will be committed each time, and it will not be possible to roll back transactions using Savepoint. - -```scala -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") -yield savepoint.getSavepointName -``` - -**Rollback of Savepoint** - -To rollback a part of a transaction using Savepoint, rollback is performed by passing Savepoint to the `rollback` method. -If you commit the entire transaction after a partial rollback using Savepoint, the transaction after that Savepoint will not be committed. - -```scala -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.rollback(savepoint) - _ <- conn.commit() -yield -``` - -**Savepoint Release** - -To release a Savepoint, pass the Savepoint to the `releaseSavepoint` method. -After releasing a Savepoint, commit the entire transaction and the transactions after that Savepoint will be committed. - -```scala -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.releaseSavepoint(savepoint) - _ <- conn.commit() -yield -``` -## Utility Commands - -MySQL has several utility commands. ([reference](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase_utility.html)) - -LDBC provides an API for using these commands. - -| Command | Use | Support | -|----------------------|--------------------------------------------------------------------|---------| -| COM_QUIT | Tells the server that the client wants it to close the connection. | ✅ | -| COM_INIT_DB | Change the default schema of the connection | ✅ | -| COM_STATISTICS | Get a human readable string of some internal status vars. | ✅ | -| COM_DEBUG | Dump debug info to server's stdout | ❌ | -| COM_PING | Check if the server is alive | ✅ | -| COM_CHANGE_USER | Changes the user of the current connection. | ✅ | -| COM_RESET_CONNECTION | Resets the session state | ✅ | -| COM_SET_OPTION | Sets options for the current connection | ✅ | - -### COM_QUIT - -The `COM_QUIT` command is used to tell the server that the client is requesting that the connection be closed. - -In LDBC, the `close` method of `Connection` can be used to close a connection. -Using the `close` method closes the connection, so the connection cannot be used in any subsequent process. - -※ Connection` uses `Resource` to manage resources. Therefore, there is no need to use the `close` method to release resources. - -```scala -connection.use { conn => - conn.close() -} -``` - -### COM_INIT_DB - -`COM_INIT_DB` is a command to change the default schema for a connection. - -In LDBC, the default schema can be changed using the `setSchema` method of `Connection`. - -```scala -connection.use { conn => - conn.setSchema("test") -} -``` - -### COM_STATISTICS - -The `COM_STATISTICS` command is used to retrieve internal status strings in readable format. - -In LDBC, you can use the `getStatistics` method of `Connection` to get the internal status string. - -```scala -connection.use { conn => - conn.getStatistics -} -``` - -The statuses that can be obtained are as follows - -- `uptime` : the time since the server was started -- `threads` : number of clients currently connected. -- `questions` : number of queries since the server started -- `slowQueries` : number of slow queries. -- `opens` : number of table opens since the server started. -- `flushTables` : number of tables flushed since the server started. -- `openTables` : number of tables currently open. -- `queriesPerSecondAvg` : average number of queries per second. - -### COM_PING - -The `COM_PING` command is used to check if the server is alive. - -In LDBC, you can check if the server is alive using the `isValid` method of `Connection`. -It returns `true` if the server is alive, or `false` if not. - -```scala -connection.use { conn => - conn.isValid -} -``` - -### COM_CHANGE_USER - -The `COM_CHANGE_USER` command is used to change the user of the current connection. -It also resets the following connection states - -- User Variables -- Temporary tables -- Prepared statements -- etc... - -LDBC allows changing the user using the `changeUser` method of `Connection`. - -```scala -connection.use { conn => - conn.changeUser("root", "password") -} -``` - -### COM_RESET_CONNECTION - -`COM_RESET_CONNECTION` is a command to reset the session state. - -`COM_RESET_CONNECTION` is a more lightweight version of `COM_CHANGE_USER`, with almost the same functionality to clean up the session state, but with the following features - -- No re-authentication (no extra client/server exchange to do so). -- Does not close connections. - -LDBC allows you to reset the session state using the `resetServerState` method of `Connection`. - -```scala -connection.use { conn => - conn.resetServerState -} -``` - -### COM_SET_OPTION - -`COM_SET_OPTION` is a command to set options for the current connection. - -LDBC allows you to set options using the `enableMultiQueries` and `disableMultiQueries` methods of `Connection`. - -The `enableMultiQueries` method allows multiple queries to be executed at once. -If you use the `disableMultiQueries` method, you will not be able to run multiple queries at once. - -It can only be used for batch processing with Insert, Update, and Delete statements; if used with a Select statement, only the results of the first query will be returned. - -```scala -connection.use { conn => - conn.enableMultiQueries *> conn.disableMultiQueries -} -``` - -## Batch commands - -LDBC allows multiple queries to be executed at once using batch commands. -Using batch commands allows multiple queries to be executed at once, reducing the number of network round trips. - -To use batch commands, add a query using the `addBatch` method of the `Statement` or `PreparedStatement` and execute the query using the `executeBatch` method. - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} -``` - -In the above example, data for `Alice` and `Bob` can be added at once. -The query to be executed would be as follows - -```sql -INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -The return value after executing a batch command is an array of the number of rows affected by each query executed. - -In the above example, one row of data for `Alice` is added and one row of data for `Bob` is added, so the return value is `List(1, 1)`. - -After executing the batch command, the queries that have been added so far by the `addBatch` method will be cleared. - -If you want to clear them manually, use the `clearBatch` method to do so. - -Translated with www.DeepL.com/Translator (free version) - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.clearBatch() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - _ <- statement.executeBatch() - yield -} -``` - -In the above example, the data for `Alice` is not added, but the data for `Bob` is. - -### Difference between Statement and PreparedStatement - -The queries executed by the batch command may differ between a `Statement` and a `PreparedStatement`. - -When an INSERT statement is executed in a batch command using a `Statement`, multiple queries are executed at once. -However, if you run an INSERT statement in a batch command using a `PreparedStatement`, a single query will be executed. - -For example, if you run the following query in a batch command, multiple queries will be executed at once because you are using a `Statement`. - -Translated with www.DeepL.com/Translator (free version) - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} - -// Query to be executed -// INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -However, if the following query is executed in a batch command, one query will be executed because of the use of `PreparedStatement`. - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.addBatch() - _ <- statement.setString(1, "Bob") - _ <- statement.setInt(2, 30) - _ <- statement.addBatch() - result <- statement.executeBatch() - yield result -} - -// Query to be executed -// INSERT INTO users (name, age) VALUES ('Alice', 20), ('Bob', 30); -``` - -This is because if you are using `PreparedStatement`, you can set multiple parameters for a single query by using the `addBatch` method after setting the query parameters. - -## Stored Procedure Execution - -LDBC provides an API for executing stored procedures. - -To execute a stored procedure, use the `prepareCall` method of `Connection` to construct a `CallableStatement`. - -※ The stored procedures used are those described in the [official](https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-statements-callable.html) document. - -```sql -CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) -BEGIN - DECLARE z INT; - SET z = inOutParam + 1; - SET inOutParam = z; - - SELECT inputParam; - - SELECT CONCAT('zyxw', inputParam); -END -``` - -To execute the above stored procedure, the following would be used - -```scala -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - hasResult <- callableStatement.execute() - values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { - for - resultSet <- callableStatement.getResultSet().flatMap { - case Some(rs) => IO.pure(rs) - case None => IO.raiseError(new Exception("No result set")) - } - value <- resultSet.getString(1) - yield value - } - yield values // List(Some("abcdefg"), Some("zyxwabcdefg")) -} -``` - -To get the value of an output parameter (a parameter you specified as OUT or INOUT when you created the stored procedure), in JDBC you must use the various `registerOutputParameter()` methods of the CallableStatement interface to specify parameters before statement execution, while LDBC will also set parameters during query execution by simply setting them using the `setXXX` method. - -However, LDBC also allows you to specify parameters using the `registerOutputParameter()` method. - -```scala -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) - hasResult <- callableStatement.execute() - value <- callableStatement.getInt(2) - yield value // 2 -} -``` - -※ Note that if you specify an Out parameter with `registerOutParameter`, the value will be set at `Null` for the server if the parameter is not set with the `setXXX` method using the same index value. - -## Unsupported Feature - -The LDBC connector is currently an experimental feature. Therefore, the following features are not supported. -We plan to provide the features as they become available. - -- Connection Pooling -- Failover measures -- etc... diff --git a/docs/src/main/mdoc/en/index.md b/docs/src/main/mdoc/en/index.md deleted file mode 100644 index b725ba5b1..000000000 --- a/docs/src/main/mdoc/en/index.md +++ /dev/null @@ -1,127 +0,0 @@ -@@@ index - * [Table Definitions](./01-Table-Definitions.md) - * [Custom Data Type](./02-Custom-Data-Type.md) - * [Type-safe Query Builder](./03-Type-safe-Query-Builder.md) - * [Database Connection](./04-Database-Connection.md) - * [Plain SQL Queries](./05-Plain-SQL-Queries.md) - * [Generating SchemaSPY Documentation](./06-Generating-SchemaSPY-Documentation.md) - * [Schema Code Generation](./07-Schema-Code-Generation.md) - * [Performance](./08-Perdormance.md) - * [Connector](./09-Connector.md) -@@@ - -# LDBC - -Note that **LDBC** is pre-1.0 software and is still under active development. Newer versions may no longer be binary compatible with earlier versions. - -## Introduction - -Most of our application development involves the use of databases.
One way to access databases in Scala is to use JDBC, and there are several libraries in Scala that wrap this JDBC. - -- Functional DSL (slick, quill, zio-sql) -- SQL string interpolator (Anorm, doobie) - -LDBC, also a JDBC-wrapped library, is a Scala 3 library that combines aspects of each, providing a type-safe, refactorable SQL interface that can express SQL expressions on a MySQL database. - -The concept of LDBC also allows development to centralize Scala models, sql schemas, and documents by using LDBC to manage a single resource. - -This concept was influenced by [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library.
By using tapir, you can build type-safe endpoints and even generate OpenAPI documents from the endpoints you build. - -LDBC uses Scala at the database layer to allow for the same type-safe construction and to allow documentation generation using what has been constructed. - -## Why LDBC? - -Development of database-based applications requires a variety of ongoing changes. - -For example, what information in the tables built in the database should be handled by the application, what queries are best suited for data retrieval, etc. - -Adding even a single column to a table definition requires modifying the SQL file, adding properties to the corresponding model, reflecting them in the database, updating documentation, etc. - -There are many other things to consider and correct. - -It is very difficult to keep up with all the maintenance during daily development, and even maintenance omissions may occur. - -I think the approach of using plain SQL to retrieve data without mapping table information to the application model and then retrieving the data with a specified type is a very good way to go. - -This way, there is no need to build database-specific models, because developers are free to work with data when they want to retrieve it, using the type of data they want to retrieve.
I also think it is very good at handling plain queries so that you can instantly see what queries are being executed. - -However, this method does not eliminate document updates, etc. just because table information is no longer managed in the application. - -LDBC has been developed to solve some of these problems. - -- Type safety: compile-time guarantees, development-time complements, read-time information -- Declarative: Separates the form of the table definition ("What") from the database connection ("How"). -- SchemaSPY Integration: Generate documents from table descriptions -- Libraries, not frameworks: can be integrated into your stack - -With LDBC, database information must be managed by the application, but type safety, query construction, and document management can be centralized. - -Mapping models in LDBC to table definitions is very easy. - -The mapping between the properties a model has and the data types defined for its columns is also very simple. The developer simply defines the corresponding columns in the same order as the properties the model has. - -```scala mdoc:silent -import ldbc.core.* - -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) -``` - -Also, attempting to combine the wrong types will result in a compile error. - -For example, passing a column of type INT to a column related to the name property of type String held by User will result in an error. - -```shell -[error] -- [E007] Type Mismatch Error: -[error] 169 | column("name", INT), -[error] | ^^^ -[error] |Found: ldbc.core.DataType.Integer[T] -[error] |Required: ldbc.core.DataType[String] -[error] | -[error] |where: T is a type variable with constraint <: Int | Long | Option[Int | Long] -``` - -For more information on these add-ons, see [Table Definitions](/ldbc/en/01-Table-Definitions.html). - -## Quick Start - -The current version is **$version$** for **Scala $scalaVersion$**. - -@@@ vars -```scala -libraryDependencies ++= Seq( - - // Start with this one. - "$org$" %% "ldbc-core" % "$version$", - - // Then add these as needed - "$org$" %% "ldbc-dsl" % "$version$", // Plain Query Database Connection - "$org$" %% "ldbc-query-builder" % "$version$", // Type-safe query construction - "$org$" %% "ldbc-schemaspy" % "$version$", // SchemaSPY document generation -) -``` -@@@ - -For more information on how to use the sbt plugin, please refer to this [documentation](/ldbc/en/07-Schema-Code-Generation.html). - -## TODO - -- JSON data type support -- SET data type support -- Geometry data type support -- Support for CHECK constraints -- Non-MySQL database support -- Streaming Support -- ZIO module support -- Integration with other database libraries -- Test Kit -- etc... diff --git a/docs/src/main/mdoc/img/compile_create.png b/docs/src/main/mdoc/img/compile_create.png deleted file mode 100644 index d9e55433c321db2be1ab12a35ffeded8707cd7a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48533 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 diff --git a/docs/src/main/mdoc/img/compile_create_query.png b/docs/src/main/mdoc/img/compile_create_query.png deleted file mode 100644 index 41b7e7809f280df2d9ff1ca1819bad44635ada45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47649 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

5tK3;K5bc4{my?EMIVJJj5k~7}qdv98M!mZFf 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 diff --git a/docs/src/main/mdoc/img/insert_throughput.png b/docs/src/main/mdoc/img/insert_throughput.png deleted file mode 100644 index 97189de3f43284570eef5a853b059ab5c03d5864..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53539 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 JEfd!B|36Z(3X1>$ diff --git a/docs/src/main/mdoc/img/lepus_logo.png b/docs/src/main/mdoc/img/lepus_logo.png deleted file mode 100644 index 076140c6b0e68b3e58e13e74cc25ab9f1bcf609a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11636 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 diff --git a/docs/src/main/mdoc/img/runtime_create.png b/docs/src/main/mdoc/img/runtime_create.png deleted file mode 100644 index f6e9b1978b47a28cf24e6c95c84e0aeb5c8167e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42249 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 O5O})!xvXc&di-v}VFDvs{6%Fl5CK?+0 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%0Dx79NZ8mJb?B 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 MI>vX)wVu5EFK - LDBC - - -[![Continuous Integration](https://github.com/takapi327/ldbc/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/takapi327/ldbc/actions/workflows/ci.yml) -[![MIT License](https://img.shields.io/badge/license-MIT-green)](https://en.wikipedia.org/wiki/MIT_License) -[![Scala Version](https://img.shields.io/badge/scala-v3.3.x-red)](https://github.com/lampepfl/dotty) -[![Typelevel Affiliate Project](https://img.shields.io/badge/typelevel-affiliate%20project-FF6169.svg)](https://typelevel.org/projects/affiliate/) -[![javadoc](https://javadoc.io/badge2/io.github.takapi327/ldbc-dsl_3/javadoc.svg)](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) -[![Maven Central Version](https://maven-badges.herokuapp.com/maven-central/io.github.takapi327/ldbc-dsl_3/badge.svg?color=blue)](https://search.maven.org/artifact/io.github.takapi327/ldbc-dsl_3/0.3.0-beta4/jar) -[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue)](https://index.scala-lang.org/takapi327/ldbc) -[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue&targetType=js)](https://index.scala-lang.org/takapi327/ldbc) -[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue&targetType=native)](https://index.scala-lang.org/takapi327/ldbc) - -ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effect 3 and Scala 3. - -ldbc is Created under the influence of [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. Using tapir, you can build type-safe endpoints and also generate OpenAPI documentation from the endpoints you build. - -ldbc allows the same type-safe construction with Scala at the database layer and document generation using the constructed one. - -Note that **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. - -## Modules availability - -ldbc is available on the JVM, Scala.js, and ScalaNative - -| Module / Platform | JVM | Scala Native | Scala.js | -|----------------------|:---:|:------------:|:--------:| -| `ldbc-core` | ✅ | ✅ | ✅ | -| `ldbc-sql` | ✅ | ✅ | ✅ | -| `ldbc-connector` | ✅ | ✅ | ✅ | -| `jdbc-connector` | ✅ | ❌ | ❌ | -| `ldbc-dsl` | ✅ | ✅ | ✅ | -| `ldbc-query-builder` | ✅ | ✅ | ✅ | -| `ldbc-schema` | ✅ | ✅ | ✅ | -| `ldbc-schemaSpy` | ✅ | ❌ | ❌ | -| `ldbc-codegen` | ✅ | ✅ | ✅ | -| `ldbc-hikari` | ✅ | ❌ | ❌ | -| `ldbc-plugin` | ✅ | ❌ | ❌ | - -## Quick Start - -For people that want to skip the explanations and see it action, this is the place to start! - -### Dependency Configuration - -```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-dsl" % "${version}" -``` - -For Cross-Platform projects (JVM, JS, and/or Native): - -```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-dsl" % "${version}" -``` - -The dependency package used depends on whether the database connection is made via a connector using the Java API or a connector provided by ldbc. - -**Use jdbc connector** - -```scala -libraryDependencies += "io.github.takapi327" %% "jdbc-connector" % "${version}" -``` - -**Use ldbc connector** - -```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-connector" % "${version}" -``` - -For Cross-Platform projects (JVM, JS, and/or Native) - -```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-connector" % "${version}" -``` - -### Usage - -The difference in usage is that there are differences in the way connections are built between jdbc and ldbc. - -> [!CAUTION] -> **ldbc** is currently under active development. Please note that current functionality may therefore be deprecated or changed in the future. - -**jdbc connector** - -```scala -val ds = new com.mysql.cj.jdbc.MysqlDataSource() -ds.setServerName("127.0.0.1") -ds.setPortNumber(13306) -ds.setDatabaseName("world") -ds.setUser("ldbc") -ds.setPassword("password") - -val datasource = jdbc.connector.MysqlDataSource[IO](ds) - -val connection: Resource[IO, Connection[IO]] = - Resource.make(datasource.getConnection)(_.close()) -``` - -**ldbc connector** - -```scala -val connection: Resource[IO, Connection[IO]] = - ldbc.connector.Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "ldbc", - password = Some("password"), - database = Some("ldbc"), - ssl = SSL.Trusted - ) -``` - -The connection process to the database can be carried out using the connections established by each of these methods. - -```scala -val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn => - (for - result1 <- sql"SELECT 1".toList[Int] - result2 <- sql"SELECT 2".headOption[Int] - result3 <- sql"SELECT 3".unsafe[Int] - yield (result1, result2, result3)).readOnly(conn) -} -``` - -#### Using the query builder - -ldbc provides not only plain queries but also type-safe database connections using the query builder. - -The first step is to create a schema for use by the query builder. - -ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in definition order. Table definitions are very similar to the structure of Create statements. This makes the construction of table definitions intuitive for the user. - -```scala -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) -``` - -The next step is to build a TableQuery using the schema you have created. - -```scala -import ldbc.query.builder.TableQuery - -val userQuery = TableQuery[User](table) -``` - -Finally, you can use the query builder to create a query. - -```scala -val result: IO[List[User]] = connection.use { conn => - userQuery.selectAll.toList[User].readOnly(conn) - // "SELECT `id`, `name`, `age` FROM user" -} -``` - -#### Using the schema - -ldbc also allows type-safe construction of schema information for tables. - -The first step is to set up dependencies. - -```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-schema" % "${version}" -``` - -For Cross-Platform projects (JVM, JS, and/or Native): - -```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-schema" % "${version}" -``` - -The next step is to create a schema for use by the query builder. - -ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in definition order. Table definitions are very similar to the structure of Create statements. This makes the construction of table definitions intuitive for the user. - -```scala -import ldbc.schema.* - -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val userTable = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) // ) -``` - -Finally, you can use the query builder to create a query. - -```scala -val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query[User].to[List].readOnly(conn) - // "SELECT `id`, `name`, `age` FROM user" -} -``` - -## Documentation - -Full documentation can be found at Currently available in English and Japanese. - -- [English](/ldbc/en/index.html) -- [Japanese](/ldbc/ja/index.html) - -## Features/Roadmap - -Creating a MySQL connector project written in pure Scala3. - -JVM, JS and Native platforms are all supported. - -> [!IMPORTANT] -> **ldbc** is currently focused on developing connectors written in pure Scala3 to work with JVM, JS and Native. -> In the future, we also plan to rewrite existing functions based on a pure Scala3 connector. - -### Enhanced functionality and improved stability of the MySQL connector written in pure Scala3 - -Most of the jdbc functionality used in other packages of ldbc at the moment could be implemented. - -However, not all jdbc APIs could be supported. Nor can we guarantee that it is proven and stable enough to operate in a production environment. - -We will continue to develop features and improve the stability of the ldbc connector to achieve the same level of stability and reliability as the jdbc connector. - -#### Connection pooling implementation - -- [ ] Failover Countermeasures - -#### Performance Verification - -- [ ] Comparison with JDBC -- [ ] Comparison with other MySQL Scala libraries -- [ ] Verification of operation in AWS and other infrastructure environments - -#### Other - -- [ ] Additional streaming implementation -- [ ] Integration with java.sql API -- [ ] etc... - -### Redesign of query builders and schema definitions - -Initially, ldbc was inspired by tapir to create a development system that could centralise Scala models, sql schemas and documentation by managing a single resource at the database level. - -In addition, database connection, query construction and document generation were to be used in combination with retrofitted packages, as the aim was to be able to integrate with other database systems. - -As a result, we feel that it has become difficult for users to use because of the various configurations required to build it. - -What users originally wanted from a database connectivity library was something simpler, easier and more intuitive to use. - -Initially, ldbc aimed to create documentation from the schema, so building the schema and query builder was not as simple as it could have been, as it required a complete description of the database data types and so on. - -It was therefore decided to redesign it to make it simpler and easier to use. - -## Contributing - -All suggestions welcome :)! - -If you’d like to contribute, see the list of [issues](https://github.com/takapi327/ldbc/issues) and pick one! Or report your own. If you have an idea you’d like to discuss, that’s always a good option. - -If you have any questions about why or how it works, feel free to ask on github. This probably means that the documentation, scaladocs, and code are unclear and can be improved for the benefit of all. - -### Testing locally - -If you want to build and run the tests for yourself, you'll need a local MySQL database. The easiest way to do this is to run `docker-compose up` from the project root. diff --git a/docs/src/main/mdoc/ja/01-Table-Definitions.md b/docs/src/main/mdoc/ja/01-Table-Definitions.md deleted file mode 100644 index 5b8b62612..000000000 --- a/docs/src/main/mdoc/ja/01-Table-Definitions.md +++ /dev/null @@ -1,406 +0,0 @@ -# テーブル定義 - -この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、[code generator](/ldbc/ja/07-Schema-Code-Generation.html) を使ってこの作業を省略することもできます。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import ldbc.core.* -import ldbc.core.attribute.* -``` - -LDBCは、Scalaモデルとデータベースのテーブル定義を1対1のマッピングで管理します。モデルが保持するプロパティとテーブルが保持するカラムのマッピングは、定義順に行われます。テーブル定義は、Create文の構造と非常によく似ています。このため、テーブル定義の構築はユーザーにとって直感的なものとなります。 - -LDBC は、このテーブル定義をさまざまな目的で使用します。型安全なクエリの生成、ドキュメントの生成など。 - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) // ); -``` - -すべてのカラムはcolumnメソッドで定義されます。各カラムにはカラム名、データ型、属性があります。以下のプリミティブ型が標準でサポートされており、すぐに使用できます。 - -- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` -- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` -- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` -- String -- Boolean -- java.time.* - -Null可能な列はOption[T]で表現され、Tはサポートされるプリミティブ型の1つです。Option型でない列はすべてNot Nullであることに注意してください。 - -## データ型 - -モデルが持つプロパティのScala型とカラムが持つデータ型の対応付けは、定義されたデータ型がScala型をサポートしている必要があります。サポートされていない型を割り当てようとするとコンパイルエラーが発生します。 - -データ型がサポートするScalaの型は以下の表の通りです。 - -| Data Type | Scala Type | -|------------|-----------------------------------------------------------------------------------------------| -| BIT | Byte, Short, Int, Long | -| TINYINT | Byte, Short | -| SMALLINT | Short, Int | -| MEDIUMINT | Int | -| INT | Int, Long | -| BIGINT | Long, BigInt | -| DECIMAL | BigDecimal | -| FLOAT | Float | -| DOUBLE | Double | -| CHAR | String | -| VARCHAR | String | -| BINARY | Array[Byte] | -| VARBINARY | Array[Byte] | -| TINYBLOB | Array[Byte] | -| BLOB | Array[Byte] | -| MEDIUMBLOB | Array[Byte] | -| LONGBLOB | Array[Byte] | -| TINYTEXT | String | -| TEXT | String | -| MEDIUMTEXT | String | -| DATE | java.time.LocalDate | -| DATETIME | java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime | -| TIMESTAMP | java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime | -| TIME | java.time.LocalTime | -| YEAR | java.time.Instant, java.time.LocalDate, java.time.Year | -| BOOLEAN | Boolean | - -**整数型を扱う際の注意点** - -符号あり、符号なしに応じて、扱えるデータの範囲がScalaの型に収まらないことに注意する必要があります。 - -| Data Type | signed range | unsigned range | Scala Type | range | -|-----------|--------------------------------------------|--------------------------|----------------|--------------------------------------------------------------------| -| TINYINT | -128 ~ 127 | 0 ~ 255 | Byte
Short | -128 ~ 127
-32768~32767 | -| SMALLINT | -32768 ~ 32767 | 0 ~ 65535 | Short
Int | -32768~32767
-2147483648~2147483647 | -| MEDIUMINT | -8388608 ~ 8388607 | 0 ~ 16777215 | Int | -2147483648~2147483647 | -| INT | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | Int
Long | -2147483648~2147483647
-9223372036854775808~9223372036854775807 | -| BIGINT | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 18446744073709551615 | Long
BigInt | -9223372036854775808~9223372036854775807
... | - -ユーザー定義の独自型やサポートされていない型を扱う場合は、[カスタム型](/ldbc/ja/02-Custom-Data-Type.html) を参照してください。 - -## 属性 - -カラムにはさまざまな属性を割り当てることができます。 - -- `AUTO_INCREMENT` - DDL文を作成し、SchemaSPYを文書化する際に、列を自動インクリメント・キーとしてマークする。 - MySQLでは、データ挿入時にAutoIncでないカラムを返すことはできません。そのため、必要に応じて、LDBCは戻りカラムがAutoIncとして適切にマークされているかどうかを確認します。 -- `PRIMARY_KEY` - DDL文やSchemaSPYドキュメントを作成する際に、列を主キーとしてマークする。 -- `UNIQUE_KEY` - DDL文やSchemaSPYドキュメントを作成する際に、列を一意キーとしてマークする。 -- `COMMENT` - DDL文やSchemaSPY文書を作成する際に、列にコメントを設定する。 - -## キーの設定 - -MySQLではテーブルに対してUniqueキーやIndexキー、外部キーなどの様々なキーを設定することができます。LDBCで構築したテーブル定義でこれらのキーを設定する方法を見ていきましょう。 - -### PRIMARY KEY - -主キー(primary key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムにプライマリーキー制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。また NULL も格納することができません。その結果、プライマリーキー制約が設定されたカラムの値を検索することで、テーブルの中でただ一つのデータを特定することができます。 - -LDBCではこのプライマリーキー制約を2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`PRIMARY_KEY`を渡すだけです。これによって以下の場合 `id`カラムを主キーとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) -``` - -**tableのkeySetメソッドで設定する** - -LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`PRIMARY_KEY`に主キーとして設定したいカラムを渡すことで主キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => PRIMARY_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`) -// ) -``` - -`PRIMARY_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### 複合キー (primary key) - -1つのカラムだけではなく、複数のカラムを主キーとして組み合わせ主キーとして設定することもできます。`PRIMARY_KEY`に主キーとして設定したいカラムを複数渡すだけで複合主キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => PRIMARY_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`PRIMARY_KEY`でしか設定することはできません。仮に以下のようにcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを主キーとして設定されてしまいます。 - -LDBCではテーブル定義に複数`PRIMARY_KEY`を設定したとしてもコンパイルエラーにすることはできません。しかし、テーブル定義をクエリの生成やドキュメントの生成などで使用する場合エラーとなります。これはPRIMARY KEYはテーブルごとに1つしか設定することができないという制約によるものです。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255), PRIMARY_KEY), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - -// CREATE TABLE `user` ( -// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, -// ) -``` - -### UNIQUE KEY - -一意キー(unique key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムに一意性制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。 - -LDBCではこの一意性制約を2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`UNIQUE_KEY`を渡すだけです。これによって以下の場合 `id`カラムを一意キーとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) -``` - -**tableのkeySetメソッドで設定する** - -LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`UNIQUE_KEY`に一意キーとして設定したいカラムを渡すことで一意キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => UNIQUE_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`) -// ) -``` - -`UNIQUE_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### 複合キー (unique key) - -1つのカラムだけではなく、複数のカラムを一意キーとして組み合わせ一意キーとして設定することもできます。`UNIQUE_KEY`に一意キーとして設定したいカラムを複数渡すだけで複合一意キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => UNIQUE_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`UNIQUE_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを一意キーとして設定されてしまいます。 - -### INDEX KEY - -インデックスキー(index key)とはMySQLにおいて目的のレコードを効率よく取得するための「索引」のことです。 - -LDBCではこのインデックスを2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`INDEX_KEY`を渡すだけです。これによって以下の場合 `id`カラムをインデックスとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) -``` - -**tableのkeySetメソッドで設定する** - -LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`INDEX_KEY`にインデックスとして設定したいカラムを渡すことでインデックスキーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => INDEX_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`) -// ) -``` - -`INDEX_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String -- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH -- `Index Option` ldbc.core.Index.IndexOption - -#### 複合キー (index key) - -1つのカラムだけではなく、複数のカラムをインデックスキーとして組み合わせインデックスキーとして設定することもできます。`INDEX_KEY`にインデックスキーとして設定したいカラムを複数渡すだけで複合インデックスとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) -) - .keySet(table => INDEX_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`INDEX_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合インデックスとしてではなく、それぞれをインデックスキーとして設定されてしまいます。 - -### FOREIGN KEY - -外部キー(foreign key)とは、MySQLにおいてデータの整合性を保つための制約(参照整合性制約)です。 外部キーに設定されているカラムには、参照先となるテーブルのカラム内に存在している値しか設定できません。 - -LDBCではこの外部キー制約をtableのkeySetメソッドを使用する方法で設定することができます。 - -```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)) -) - -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) -// ) -``` - -`FOREIGN_KEY`メソッドにはカラムとReference値意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String - -外部キー制約には親テーブルの削除時と更新時の挙動を設定することができます。`REFERENCE`メソッドに`onDelete`と`onUpdate`メソッドが提供されているのでこちらを使用することでそれぞれ設定することができます。 - -設定することのできる値は`ldbc.core.Reference.ReferenceOption`から取得することができます。 - -```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id).onDelete(Reference.ReferenceOption.RESTRICT))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT -// ) -``` - -設定することのできる値は以下になります。 - -- `RESTRICT`: 親テーブルに対する削除または更新操作を拒否します。 -- `CASCADE`: 親テーブルから行を削除または更新し、子テーブル内の一致する行を自動的に削除または更新します。 -- `SET_NULL`: 親テーブルから行を削除または更新し、子テーブルの外部キーカラムを NULL に設定します。 -- `NO_ACTION`: 標準 SQL のキーワード。 MySQLでは、RESTRICT と同等です。 -- `SET_DEFAULT`: このアクションは MySQL パーサーによって認識されますが、InnoDB と NDB はどちらも、ON DELETE SET DEFAULT または ON UPDATE SET DEFAULT 句を含むテーブル定義を拒否します。 - -#### 複合キー (foreign key) - -1つのカラムだけではなく、複数のカラムを外部キーとして組み合わせて設定することもできます。`FOREIGN_KEY`に外部キーとして設定したいカラムを複数渡すだけで複合外部キーとして設定することができます。 - -```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("category", SMALLINT[Short]) -) - -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]), - column("post_category", SMALLINT[Short]) -) - .keySet(table => FOREIGN_KEY((table.postId, table.postCategory), REFERENCE(post, (post.id, post.category)))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`post_id`, `post_category`) REFERENCES `post` (`id`, `category`) -// ) -``` - -### 制約名 - -MySQLではCONSTRAINTを使用することで制約に対して任意の名前を付与することができます。この制約名はデータベース単位で一意の値である必要があります。 - -LDBCではCONSTRAINTメソッドが提供されているのでキー制約などの制約を設定する処理をCONSTRAINTメソッドに渡すだけで設定することができます。 - -```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) -) - .keySet(table => CONSTRAINT("fk_post_id", FOREIGN_KEY(table.postId, REFERENCE(post, post.id)))) - -// CREATE TABLE `user` ( -// ..., -// CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) -// ) -``` diff --git a/docs/src/main/mdoc/ja/02-Custom-Data-Type.md b/docs/src/main/mdoc/ja/02-Custom-Data-Type.md deleted file mode 100644 index 8a3c9ea06..000000000 --- a/docs/src/main/mdoc/ja/02-Custom-Data-Type.md +++ /dev/null @@ -1,78 +0,0 @@ -# カスタム データ型 - -この章では、LDBCで構築したテーブル定義でユーザー独自の型もしくはサポートされていない型を使用するための方法について説明します。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import ldbc.core.* -``` - -ユーザー独自の型もしくはサポートされていない型を使用するための方法はカラムのデータ型をどのような型として扱うかを教えてあげることです。DataTypeには`mapping`メソッドが提供されているのでこのメソッドを使用して暗黙の型変換として設定します。 - -```scala 3 -case class User( - id: Long, - name: User.Name, - age: Option[Int], -) - -object User: - - case class Name(firstName: String, lastName: String) - - given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` - -LDBCでは複数のカラムをモデルが持つ1つのプロパティに統合することはできません。LDBCの目的はモデルとテーブルを1対1でマッピングを行い、データベースのテーブル定義を型安全に構築することにあるからです。 - -そのためテーブル定義とモデルで異なった数のプロパティを持つようなことは許可していません。以下のような実装はコンパイルエラーとなります。 - -```scala 3 -case class User( - id: Long, - name: User.Name, - age: Option[Int], -) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` - -上記のような実装を行いたい場合は以下のような実装を検討してください。 - -```scala 3 -case class User( - id: Long, - firstName: String, - lastName: String, - age: Option[Int], -): - - val name: User.Name = User.Name(firstName, lastName) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) - ) -``` diff --git a/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md b/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md deleted file mode 100644 index 1f1c4c73c..000000000 --- a/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md +++ /dev/null @@ -1,486 +0,0 @@ -# 型安全なクエリ構築 - -この章では、LDBCで構築したテーブル定義を使用して、型安全にクエリを構築するための方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-query-builder" % "$version$" -``` -@@@ - -LDBCでのテーブル定義方法をまだ読んでいない場合は、[テーブル定義](/ldbc/ja/01-Table-Definitions.html)の章を先に読むことをオススメしましす。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import cats.effect.IO -import ldbc.core.* -import ldbc.query.builder.TableQuery -``` - -LDBCではTableQueryにテーブル定義を渡すことで型安全なクエリ構築を行います。 - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) - -val userQuery = TableQuery[User](table) -``` - -## SELECT - -型安全にSELECT文を構築する方法はTableQueryが提供する`select`メソッドを使用することです。LDBCではプレーンなクエリに似せて実装されているため直感的にクエリ構築が行えます。またどのようなクエリが構築されているかも一目でわかるような作りになっています。 - -特定のカラムのみ取得を行うSELECT文を構築するには`select`メソッドで取得したいカラムを指定するだけです。 - -```scala 3 -val select = userQuery.select(_.id) - -select.statement === "SELECT `id` FROM user" -``` - -複数のカラムを指定する場合は`select`メソッドで取得したいカラムを指定して指定したカラムのタプルを返すだけです。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name)) - -select.statement === "SELECT `id`, `name` FROM user" -``` - -全てのカラムを指定したい場合はTableQueryが提供する`selectAll`メソッドを使用することで構築できます。 - -```scala 3 -val select = userQuery.selectAll - -select.statement === "SELECT `id`, `name`, `age` FROM user" -``` - -特定のカラムの数を取得したい場合は、指定したカラムで`count`を使用することで構築できます。  - -```scala 3 -val select = userQuery.select(_.id.count) - -select.statement === "SELECT COUNT(id) FROM user" -``` - -### WHERE - -クエリに型安全にWhere条件を設定する方法は`where`メソッドを使用することです。 - -```scala 3 -val select = userQuery.select(_.id).where(_.name === "Test") - -select.statement === "SELECT `id` FROM user WHERE name = ?" -``` - -`where`メソッドで使用できる条件の一覧は以下です。 - -| 条件 | ステートメント | -|--------------------------------------|---------------------------------------| -| === | `column = ?` | -| >= | `column >= ?` | -| > | `column > ?` | -| <= | `column <= ?` | -| < | `column < ?` | -| <> | `column <> ?` | -| !== | `column != ?` | -| IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL") | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | -| <=> | `column <=> ?` | -| IN (value, value, ...) | `column IN (?, ?, ...)` | -| BETWEEN (start, end) | `column BETWEEN ? AND ?` | -| LIKE (value) | `column LIKE ?` | -| LIKE_ESCAPE (like, escape) | `column LIKE ? ESCAPE ?` | -| REGEXP (value) | `column REGEXP ?` | -| `<<` (value) | `column << ?` | -| `>>` (value) | `column >> ?` | -| DIV (cond, result) | `column DIV ? = ?` | -| MOD (cond, result) | `column MOD ? = ?` | -| ^ (value) | `column ^ ?` | -| ~ (value) | `~column = ?` | - -### GROUP BY/Having - -クエリに型安全にGROUP BY句を設定する方法は`groupBy`メソッドを使用することです。 - -`groupBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準にグループ化することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3) - -select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age" -``` - -グループ化すると`select`で取得できるデータの数はグループの数だけとなります。そこでグループ化を行った場合には、グループ化に指定したカラムの値や、用意された関数を使ってカラムの値をグループ単位で集計した結果などを取得することができます。 - -`having`を使用すると`groupBy`によってグループ化されて取得したデータに関して、取得する条件を設定することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3).having(_._3 > 20) - -select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age HAVING age > ?" -``` - -### ORDER BY - -クエリに型安全にORDER BY句を設定する方法は`orderBy`メソッドを使用することです。 - -`orderBy`を使うことで`select`でデータを取得する時に指定したカラムの値を対象にソートした結果を取得することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age) - -select.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age" -``` - -昇順/降順を指定したい場合は、それぞれカラムに対して `asc`/`desc`を呼び出すだけです。 - -```scala 3 -val desc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.desc) - -desc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age DESC" - -val asc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.asc) - -asc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age ASC" -``` - -### LIMIT/OFFSET - -クエリに型安全にLIMIT句とOFFSET句を設定する方法は`limit`/`offset`メソッドを使用することです。 - -`limit`を設定すると`select`を実行した時に取得するデータの行数の上限を設定することができ、`offset`を設定すると何番目からのデータを取得するのかを指定することができます。 - -```scala 3 -val select = userQuery.select(user => (user.id, user.name, user.age)).limit(100).offset(50) - -select.statement === "SELECT `id`, `name`, `age` FROM user LIMIT ? OFFSET ?" -``` - -## JOIN/LEFT JOIN/RIGHT JOIN - -クエリに型安全にJoinを設定する方法は`join`/`leftJoin`/`rightJoin`メソッドを使用することです。 - -Joinでは以下定義をサンプルとして使用します。 - -```scala 3 -case class Country(code: String, name: String) -object Country: - val table = Table[Country]("country")( - column("code", CHAR(3), PRIMARY_KEY), - column("name", VARCHAR(255)) - ) - -case class City(id: Long, name: String, countryCode: String) -object City: - val table = Table[City]("city")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("country_code", CHAR(3)) - ) - -case class CountryLanguage( - countryCode: String, - language: String -) -object CountryLanguage: - val table: Table[CountryLanguage] = Table[CountryLanguage]("country_language")( - column("country_code", CHAR(3)), - column("language", CHAR(30)) - ) - -val countryQuery = TableQuery[Country](Country.table) -val cityQuery = TableQuery[City](City.table) -val countryLanguageQuery = TableQuery[CountryLanguage](CountryLanguage.table) -``` - -まずシンプルなJoinを行いたい場合は、`join`を使用します。 -`join`の第一引数には結合したいテーブルを渡し、第二引数では結合元のテーブルと結合したいテーブルのカラムで比較を行う関数を渡します。これはJoinにおいてのON句に該当します。 - -Join後の`select`は2つのテーブルからカラムを指定することになります。 - -```scala 3 -val join = countryQuery.join(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country JOIN city ON country.code = city.country_code" -``` - -次に左外部結合であるLeft Joinを行いたい場合は、`leftJoin`を使用します。 -`join`が`leftJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 - -```scala 3 -val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city ON country.code = city.country_code" -``` - -シンプルなJoinとの違いは`leftJoin`を使用した場合、結合を行うテーブルから取得するレコードはNULLになる可能性があるということです。 - -そのためLDBCでは`leftJoin`に渡されたテーブルから取得するカラムのレコードは全てOption型になります。 - -```scala 3 -val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (String, Option[String]) -``` - -次に右外部結合であるRight Joinを行いたい場合は、`rightJoin`を使用します。 -こちらも`join`が`rightJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 - -```scala 3 -val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) - -join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN city ON country.code = city.country_code" -``` - -シンプルなJoinとの違いは`rightJoin`を使用した場合、結合元のテーブルから取得するレコードはNULLになる可能性があるということです。 - -そのためLDBCでは`rightJoin`を使用した結合元のテーブルから取得するカラムのレコードは全てOption型になります。 - -```scala 3 -val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (Option[String], String) -``` - -複数のJoinを行いたい場合は、メソッドチェーンで任意のJoinメソッドを呼ぶことで実現することができます。 - -```scala 3 -val join = - (countryQuery join cityQuery)((country, city) => country.code === city.countryCode) - .rightJoin(countryLanguageQuery)((_, city, countryLanguage) => city.countryCode === countryLanguage.countryCode) - .select((country, city, countryLanguage) => (country.name, city.name, countryLanguage.language)) // (Option[String], Option[String], String)] - -join.statement = - """ - |SELECT - | country.`name`, - | city.`name`, - | country_language.`language` - |FROM country - |JOIN city ON country.code = city.country_code - |RIGHT JOIN country_language ON city.country_code = country_language.country_code - |""".stripMargin -``` - -複数のJoinを行っている状態で`rightJoin`での結合を行うと、今までの結合が何であったかにかかわらず直前まで結合していたテーブルから取得するレコードは全てNULL許容なアクセスとなることに注意してください。 - -## Custom Data Type - -前章でユーザー独自の型もしくはサポートされていない型を使用するためにDataTypeの`mapping`メソッドを使用して独自の型とDataTypeのマッピングを行ないました。([参照](/ldbc/ja/02-Custom-Data-Type.html)) - -LDBCはテーブル定義とデータベースへの接続処理が分離されています。 -そのためデータベースからデータを取得する際にユーザー独自の型もしくはサポートされていない型に変換したい場合は、ResultSetからのデータ取得方法を独自の型もしくはサポートされていない型と紐付けてあげる必要があります。 - -例えばユーザー定義のEnumを文字列型とマッピングしたい場合は、以下のようになります。 - -```scala 3 -enum Custom: - case ... - -given ResultSetReader[IO, Custom] = - ResultSetReader.mapping[IO, str, Custom](str => Custom.valueOf(str)) -``` - -※ この処理は将来のバージョンでDataTypeのマッピングと統合される可能性があります。 - -## INSERT - -型安全にINSERT文を構築する方法はTableQueryが提供する以下のメソッドを使用することです。 - -- insert -- insertInto -- += -- ++= - -**insert** - -`insert`メソッドには挿入するデータのタプルを渡します。タプルはモデルと同じプロパティの数と型である必要があります。また、挿入されるデータの順番はモデルのプロパティおよびテーブルのカラムと同じ順番である必要があります。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" -``` - -複数のデータを挿入したい場合は、`insert`メソッドに複数のタプルを渡すことで構築できます。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None), (2L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -**insertInto** - -`insert`メソッドはテーブルが持つ全てのカラムにデータ挿入を行いますが、特定のカラムに対してのみデータを挿入したい場合は`insertInto`メソッドを使用します。 - -これはAutoIncrementやDefault値を持つカラムへのデータ挿入を除外したい場合などに使用できます。 - -```scala 3 -val insert = userQuery.insertInto(user => (user.name, user.age)).values(("name", None)) - -insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?)" -``` - -複数のデータを挿入したい場合は、`values`にタプルの配列を渡すことで構築できます。 - -```scala 3 -val insert = userQuery.insertInto(user => (user.name, user.age)).values(List(("name", None), ("name", Some(20)))) - -insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?), (?, ?)" -``` - -**+=** - -`+=`メソッドを使用することでモデルを使用してinsert文を構築することができます。モデルを使用する場合は全てのカラムにデータを挿入してしまうことに注意してください。 - -```scala 3 -val insert = userQuery += User(1L, "name", None) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" -``` - -**++=** - -モデルを使用して複数のデータを挿入したい場合は`++=`メソッドを使用します。 - -```scala 3 -val insert = userQuery ++= List(User(1L, "name", None), User(2L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -### ON DUPLICATE KEY UPDATE - -ON DUPLICATE KEY UPDATE 句を指定し行を挿入すると、UNIQUEインデックスまたはPRIMARY KEYで値が重複する場合、古い行のUPDATEが発生します。 - -LDBCでこの処理を実現する方法は2種類あり、`insertOrUpdate{s}`を使用するか、`Insert`に対して`onDuplicateKeyUpdate`を使用することです。 - -```scala 3 -val insert = userQuery.insertOrUpdate((1L, "name", None)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `id` = new_user.`id`, `name` = new_user.`name`, `age` = new_user.`age`" -``` - -`insertOrUpdate{s}`を使用した場合、全てのカラムが更新対象となることに注意してください。重複する値があり特定のカラムのみを更新したい場合は、`onDuplicateKeyUpdate`を使用して更新したいカラムのみを指定するようにしてください。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).onDuplicateKeyUpdate(v => (v.name, v.age)) - -insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `age` = new_user.`age`" -``` - -## UPDATE - -型安全にUPDATE文を構築する方法はTableQueryが提供する`update`メソッドを使用することです。 - -`update`メソッドの第1引数にはテーブルのカラム名ではなくモデルのプロパティ名を指定し、第2引数に更新したい値を渡します。第2引数に渡す値の型は第1引数で指定したプロパティの型と同じである必要があります。 - -```scala 3 -val update = userQuery.update("name", "update name") - -update.statement === "UPDATE user SET name = ?" -``` - -第1引数に存在しないプロパティ名を指定した場合コンパイルエラーとなります。 - -```scala 3 -val update = userQuery.update("hoge", "update name") // Compile error -``` - -複数のカラムを更新したい場合は`set`メソッドを使用します。 - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20)) - -update.statement === "UPDATE user SET name = ?, age = ?" -``` - -`set`メソッドには条件に応じてクエリを生成させないようにすることもできます。 - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20), false) - -update.statement === "UPDATE user SET name = ?" -``` - -モデルを使用してupdate文を構築することもできます。モデルを使用する場合は全てのカラムを更新してしまうことに注意してください。 - -```scala 3 -val update = userQuery.update(User(1L, "update name", None)) - -update.statement === "UPDATE user SET id = ?, name = ?, age = ?" -``` - -### WHERE - -`where`メソッドを使用することでupdate文にもWhere条件を設定することができます。 - -```scala 3 -val update = userQuery.update("name", "update name").set("age", Some(20)).where(_.id === 1) - -update.statement === "UPDATE user SET name = ?, age = ? WHERE id = ?" -``` - -`where`メソッドで使用できる条件はInsert文の[where項目](/ldbc/ja/03-Type-safe-Query-Builder.html#where)を参照してください。 - -## DELETE - -型安全にDELETE文を構築する方法はTableQueryが提供する`delete`メソッドを使用することです。 - -```scala 3 -val delete = userQuery.delete - -delete.statement === "DELETE FROM user" -``` - -### WHERE - -`where`メソッドを使用することでdelete文にもWhere条件を設定することができます。 - -```scala 3 -val delete = userQuery.delete.where(_.id === 1) - -delete.statement === "DELETE FROM user WHERE id = ?" -``` - -`where`メソッドで使用できる条件はInsert文の[where項目](/ldbc/ja/03-Type-safe-Query-Builder.html#where)を参照してください。 - -## DDL - -型安全にDDLを構築する方法はTableQueryが提供する以下のメソッドを使用することです。 - -- createTable -- dropTable -- truncateTable - -spec2を使用している場合は以下のようにしてテストの前後にDDLを実行することができます。 - -```scala 3 -import cats.effect.IO -import cats.effect.unsafe.implicits.global - -import org.specs2.mutable.Specification -import org.specs2.specification.core.Fragments -import org.specs2.specification.BeforeAfterEach - -object Test extends Specification, BeforeAfterEach: - - override def before: Fragments = - step((tableQuery.createTable.update.autoCommit(dataSource) >> IO.println("Complete create table")).unsafeRunSync()) - - override def after: Fragments = - step((tableQuery.dropTable.update.autoCommit(dataSource) >> IO.println("Complete drop table")).unsafeRunSync()) -``` diff --git a/docs/src/main/mdoc/ja/04-Database-Connection.md b/docs/src/main/mdoc/ja/04-Database-Connection.md deleted file mode 100644 index 102598b5c..000000000 --- a/docs/src/main/mdoc/ja/04-Database-Connection.md +++ /dev/null @@ -1,410 +0,0 @@ -# データベース接続 - -この章では、LDBCで構築したクエリを使用して、データベースへの接続処理を行うための方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -@@@ vars -```scala -libraryDependencies ++= Seq( - "$org$" %% "ldbc-dsl" % "$version$", - "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" -) -``` -@@@ - -LDBCでのクエリ構築方法をまだ読んでいない場合は、[型安全なクエリ構築](/ldbc/ja/03-Type-safe-Query-Builder.html)の章を先に読むことをオススメしましす。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import com.mysql.cj.jdbc.MysqlDataSource - -import cats.effect.IO -// This is just for testing. Consider using cats.effect.IOApp instead of calling -// unsafe methods directly. -import cats.effect.unsafe.implicits.global - -import ldbc.sql.* -import ldbc.dsl.io.* -import ldbc.dsl.logging.ConsoleLogHandler -import ldbc.query.builder.TableQuery -``` - -テーブル定義は以下を使用します。 - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) - -val userQuery = TableQuery[User](table) -``` - -## DataSourceの使用 - -LDBCはデータベース接続にJDBCのDataSourceを使用します。LDBCにはこのDataSourceを構築する実装は提供されていないため、mysqlやHikariCPなどのライブラリを使用する必要があります。今回の例ではMysqlDataSourceを使用してDataSourceの構築を行います。 - -```scala 3 -private val dataSource = new MysqlDataSource() -dataSource.setServerName("127.0.0.1") -dataSource.setPortNumber(3306) -dataSource.setDatabaseName("database name") -dataSource.setUser("user name") -dataSource.setPassword("password") -``` - -## ログ - -LDBCではDatabase接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 - -標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 - -```scala 3 -given LogHandler[IO] = ConsoleLogHandler[IO] -``` - -### カスタマイズ - -任意のロギングライブラリを使用してログをカスタマイズする場合は`ldbc.dsl.logging.LogHandler`を使用します。 - -以下は標準実装のログ実装です。LDBCではデータベース接続で以下3種類のイベントが発生します。 - -- Success: 処理の成功 -- ProcessingFailure: データ取得後もしくはデータベース接続前の処理のエラー -- ExecFailure: データベースへの接続処理のエラー - -それぞれのイベントでどのようなログを書き込むかをパターンマッチングによって振り分けを行います。 - -```scala 3 -def consoleLogger[F[_]: Console: Sync]: LogHandler[F] = - case LogEvent.Success(sql, args) => - Console[F].println( - s"""Successful Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) - case LogEvent.ProcessingFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed ResultSet Processing: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) - case LogEvent.ExecFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) -``` - -## Query - -`select`文を構築すると`toList`/`headOption`/`unsafe`メソッドを使用できるようになります。これらのメソッドは取得後のデータ形式を決定するために使用します。特段何も型を指定しない場合は`select`メソッドで指定したカラムの型がTupleとして返却されます。 - -### toList - -クエリを実行した結果データの一覧を取得したい場合は、`toList`メソッドを使用します。`toList`メソッドを使用してデータベース処理を行なった結果、データ取得件数が0件であった場合空の配列が返されます。 - -```scala 3 -val query1 = userQuery.selectAll.toList // List[(Long, String, Option[Int])] -``` - -`toList`メソッドにモデルを指定すると取得後のデータを指定したモデルに変換することができます。 - -```scala 3 -val query = userQuery.selectAll.toList[User] // User -``` - -`toList`メソッドで指定するモデルの型は`select`メソッドで指定したTupleの型と一致するか、Tupleの型から指定したモデルへの型変換が可能なものでなければなりません。 - -```scala 3 -val query1 = userQuery.select(user => (user.name, user.age)).toList[User] // Compile error - -case class Test(name: String, age: Option[Int]) -val query2 = userQuery.select(user => (user.name, user.age)).toList[Test] // Test -``` - -### headOption - -クエリを実行した結果最初の1件のデータをOptionalで取得したい場合は、`headOption`メソッドを使用します。`headOption`メソッドを使用してデータベース処理を行なった結果データ取得件数が0件であった場合Noneが返されます。 - -`headOption`メソッドを使用した場合、複数のデータを取得するクエリを実行したとしても最初のデータのみ返されることに注意してください。 - -```scala 3 -val query1 = userQuery.selectAll.headOption // Option[(Long, String, Option[Int])] -val query2 = userQuery.selectAll.headOption[User] // Option[User] -``` - -### unsafe - -`unsafe`メソッドを使用した場合、取得したデータの最初の1件のみ返されることは`headOption`メソッドと同じですが、データはOptionalにはならずそのままのデータが返却されます。もし取得したデータの件数が0件であった場合は例外が発生するため適切な例外ハンドリングを行う必要があります。 - -実行時に例外を発生する可能性が高いため`unsafe`という名前になっています。 - -```scala 3 -val query1 = userQuery.selectAll.unsafe // (Long, String, Option[Int]) -val query2 = userQuery.selectAll.unsafe[User] // User -``` - -## Update - -`insert/update/delete`文を構築すると`update`メソッドを使用できるようになります。`update`メソッドはデータベースへの書き込み処理件数を返却します。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).update // Int -val update = userQuery.update("name", "update name").update // Int -val delete = userQuery.delete.update // Int -``` - -`insert`文の場合データ挿入時にAutoIncrementで生成された値を返却させたい場合があります。その場合は`update`メソッドではなく`returning`メソッドを使用して返却したいカラムを指定します。 - -```scala 3 -val insert = userQuery.insert((1L, "name", None)).returning("id") // Long -``` - -`returning`メソッドで指定する値はモデルが持つプロパティ名である必要があります。また、指定したプロパティがテーブル定義上でAutoIncrementの属性が設定されていなければエラーとなってしまいます。 - -MySQLではデータ挿入時に返却できる値はAutoIncrementのカラムのみであるため、LDBCでも同じような仕様となっています。 - -## データベース操作の実行 - -データベース接続を行う前にコミットのタイミングや読み書き専用などの設定を行う必要があります。 - -### 読み取り専用 - -`readOnly`メソッドを使用することで実行するクエリの処理を読み込み専用にすることができます。`readOnly`メソッドは`insert/update/delete`文でも使用することができますが、書き込み処理を行うので実行時にエラーとなります。 - -```scala 3 -val read = userQuery.selectAll.toList.readOnly(dataSource) -``` - -### 自動コミット - -`autoCommit`メソッドを使用することで実行するクエリの処理をクエリ実行時ごとにコミットするように設定することができます。 - -```scala 3 -val read = userQuery.insert((1L, "name", None)).update.autoCommit(dataSource) -``` - -### トランザクション - -`transaction`メソッドを使用することで複数のデータベース接続処理を1つのトランザクションにまとめることができます。 - -`toList/headOption/unsafe/returning/update`メソッドの戻り値は`Kleisli[F, Connection[F], T]`型となっています。そのためmapやflatMapを使用して処理を1つにまとめることができます。 - -1つにまとめた`Kleisli[F, Connection[F], T]`に対して`transaction`メソッドを使用することで、中で行われる全てのデータベース接続処理は1つのトランザクションにまとめて実行されます。 - -```scala 3 -(for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction(dataSource) -``` - -## Database Action - -データベース処理を実行する方法としてデータベースへの接続情報を持った`Database`を使用して行う方法も存在します。 - -`Database`を構築する方法はDriverManagerを使用した方法と、DataSourceから生成する方法の2種類があります。以下はMySQLのドライバーを使用してデータベースへの接続情報を持った`Database`を構築する例です。 - -```scala 3 -val db = Database.fromMySQLDriver[IO]("database name", "host", "port number", "user name", "password") -``` - -`Database`を使用してデータベース処理を実行するメリットは以下になります。 - -- DataSourceの構築を簡略できる (DriverManagerを使用した場合) -- クエリごとにDataSourceを受け渡す必要がなくなる - -`Database`を使用する方法は、DataSourceを受け渡す方法を簡略化しただけにすぎないため、どちらを使用しても実行結果に差が出ることはありません。 -`flatMap`などで処理を結合しメソッドチェーンで実行するか、結合した処理を`Database`を使用して実行するかの違いでしかありません。そのため実行方法はユーザーの好きの方法を選択できます。 - -**Read Only** - -```scala 3 -val user: Option[User] = db.readOnly(userQuery.selectAll.headOption[User]).unsafeRunSync() -``` - -**Auto Commit** - -```scala 3 -val result = db.autoCommit(userQuery.insert((1L, "name", None)).update).unsafeRunSync() -``` - -**Transaction** - -```scala 3 -db.transaction(for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).unsafeRunSync() -``` - -### Database model - -LDBCでは`Database`モデルはデータベースの接続情報を持つ以外の用途でも使用されます。他の用途としてSchemaSPYのドキュメント生成に使用されることです。SchemaSPYのドキュメント生成に関しては[こちら](/ldbc/ja/06-Generating-SchemaSPY-Documentation.html)を参照してください。 - -すでに`Database`モデルを別の用途で生成している場合は、そのモデルを使用してデータベースの接続情報を持った`Database`を構築することができます。 - -```scala 3 -import ldbc.dsl.io.* - -val database: Database = ??? - -val db = database.fromDriverManager() -// or -val db = database.fromDriverManager("user name", "password") -``` - -### メソッドチェーンでの使用 - -`Database`モデルは`TableQuery`のメソッドで`DataSource`の代わりに使用することもできます。 - -```scala 3 -val read = userQuery.selectAll.toList.readOnly(db) -val commit = userQuery.insert((1L, "name", None)).update.autoCommit(db) -val transaction = (for - result1 <- userQuery.insert((1L, "name", None)).returning("id") - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction(db) -``` - -## HikariCPコネクションプールの使用 - -`ldbc-hikari`は、HikariCP接続プールを構築するためのHikariConfigおよびHikariDataSourceを構築するためのビルダーを提供します。 - -@@@ vars -```scala -libraryDependencies ++= Seq( - "$org$" %% "ldbc-hikari" % "$version$", -) -``` -@@@ - -`HikariConfigBuilder`は名前の通りHikariCPの`HikariConfig`を構築するためのビルダーです。 - -```scala 3 -val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.default.build() -``` - -`HikariConfigBuilder`には`default`と`from`メソッドがあり`default`を使用した場合、LDBC指定のパスを元にConfigから対象の値を取得して`HikariConfig`の構築を行います。 - -```text -ldbc.hikari { - jdbc_url = ... - username = ... - password = ... -} -``` - -ユーザー独自のパスを指定したい場合は`from`メソッドを使用して引数に取得したいパスを渡す必要があります。 - -```scala 3 -val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.from("custom.path").build() - -// custom.path { -// jdbc_url = ... -// username = ... -// password = ... -// } -``` - -HikariCPに設定できる内容は[公式](https://github.com/brettwooldridge/HikariCP)を参照してください。 - -Configに設定できるキーの一覧は以下になります。 - -| キー名 | 説明 | 型 | -|-----------------------------|------------------------------------------------------------------------|----------| -| catalog | 接続時に設定するデフォルトのカタログ名 | String | -| connection_timeout | クライアントがプールからの接続を待機する最大ミリ秒数 | Duration | -| idle_timeout | 接続がプール内でアイドル状態であることを許可される最大時間 (ミリ秒単位) | Duration | -| leak_detection_threshold | 接続漏れの可能性を示すメッセージがログに記録されるまでに、接続がプールから外れる時間 | Duration | -| maximum_pool_size | アイドル接続と使用中の接続の両方を含め、プールが許容する最大サイズ | Int | -| max_lifetime | プール内の接続の最大寿命 | Duration | -| minimum_idle | アイドル接続と使用中接続の両方を含め、HikariCPがプール内に維持しようとするアイドル接続の最小数 | Int | -| pool_name | 接続プールの名前 | String | -| allow_pool_suspension | プール・サスペンドを許可するかどうか | Boolean | -| auto_commit | プール内の接続のデフォルトの自動コミット動作 | Boolean | -| connection_init_sql | 新しい接続が作成されたときに、その接続がプールに追加される前に実行されるSQL文字列 | String | -| connection_test_query | 接続の有効性をテストするために実行する SQL クエリ | String | -| data_source_classname | Connections の作成に使用する JDBC DataSourceの完全修飾クラス名 | String | -| initialization_fail_timeout | プール初期化の失敗タイムアウト | Duration | -| isolate_internal_queries | 内部プール・クエリ (主に有効性チェック)を、`Connection.rollback()`によって独自のトランザクションで分離するかどうか | Boolean | -| jdbc_url | JDBCのURL | String | -| readonly | プールに追加する接続を読み取り専用接続として設定するかどうか | Boolean | -| register_mbeans | HikariCPがJMXにHikariConfigMXBeanとHikariPoolMXBeanを自己登録するかどうか | Boolean | -| schema | 接続時に設定するデフォルトのスキーマ名 | String | -| username | `DataSource.getConnection(username,password)`の呼び出しに使用されるデフォルトのユーザ名 | String | -| password | `DataSource.getConnection(username,password)`の呼び出しに使用するデフォルトのパスワード | String | -| driver_class_name | 使用するDriverのクラス名 | String | -| transaction_isolation | デフォルトのトランザクション分離レベル | String | - -`HikariDataSourceBuilder`を使用することで、HikariCPの`HikariDataSource`を構築することができます。 - -接続プールはライフタイムで管理されるオブジェクトでありきれいにシャットダウンする必要があるため、ビルダーによって構築された`HikariDataSource`は`Resource`として管理されます。 - -```scala 3 -val dataSource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() -``` - -`buildDataSource`経由で構築された`HikariDataSource`は、内部でLDBC指定のパスを元にConfigから設定を取得し構築された`HikariConfig`を使用しています。 -これは`HikariConfigBuilder`の`default`経由で生成された`HikariConfig`と同等のものです。 - -もしユーザー指定の`HikariConfig`を使用したい場合は、`buildFromConfig`を使用することで`HikariDataSource`を構築することができます。 - -```scala 3 -val hikariConfig = ??? -val dataSource = HikariDataSourceBuilder.default[IO].buildFromConfig(hikariConfig) -``` - -`HikariDataSourceBuilder`を使用して構築された`HikariDataSource`は通常IOAppを使用して実行します。 - -```scala 3 -object HikariApp extends IOApp: - - val dataSourceResource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() - - def run(args: List[String]): IO[ExitCode] = - dataSourceResource.use { dataSource => - ... - } -``` - -### HikariDatabase - -HikariCPのコネクション情報を持った`Database`を構築する方法も存在します。 - -`HikariDatabase`は`HikariDataSource`と同様に`Resource`として管理されます。 -そのため通常はIOAppを使用して実行します。 - -```scala 3 -object HikariApp extends IOApp: - - val hikariConfig = ??? - val databaseResource: Resource[F, Database[F]] = HikariDatabase.fromHikariConfig[IO](hikariConfig) - - def run(args: List[String]): IO[ExitCode] = - databaseResource.use { database => - for - result <- database.readOnly(...) - yield ExitCode.Success - } -``` diff --git a/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md b/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md deleted file mode 100644 index e52030ef7..000000000 --- a/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md +++ /dev/null @@ -1,34 +0,0 @@ -# プレーンなSQLクエリ - -時には、抽象度の高いレベルではうまくサポートされていない操作のために、独自のSQLコードを書く必要があるかもしれません。JDBCの低レイヤーに戻る代わりに、ScalaベースのAPIでLDBCのPlain SQLクエリーを使うことができます。 -この章では、そのような場合にLDBCでPlain SQLクエリーを使用してデータベースへの接続処理を行うための方法について説明します。 - -プロジェクトへの依存関係やDataSourceの使用とログに関しては、前章の[データベース接続](/ldbc/ja/04-Database-Connection.html)の章を参照してください。 - -## Plain SQL - -LDBCでは以下のようにsql文字列補間をリテラルSQL文字列で使用してプレーンなクエリを構築します。 - -クエリに注入された変数や式は、結果のクエリ文字列のバインド変数に変換されます。クエリ文字列に直接挿入されるわけではないので、SQLインジェクション攻撃の危険はありません。 - -```scala 3 -val select = sql"SELECT id, name, age FROM user WHERE id = $id" // SELECT id, name, age FROM user WHERE id = ? -val insert = sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)" // INSERT INTO user (id, name, age) VALUES(?, ?, ?) -val update = sql"UPDATE user SET id = $id, name = $name, age = $age" // UPDATE user SET id = ?, name = ?, age = ? -val delete = sql"DELETE FROM user WHERE id = $id" // DELETE FROM user WHERE id = ? -``` - -Plain SQLクエリーは実行時にSQL文を構築するだけです。これは安全かつ簡単に複雑なステートメントを構築する方法を提供しますが、これは単なる埋め込み文字列にすぎません。ステートメントに構文エラーがあったり、データベースとScalaコードの型が一致しなかったりしてもコンパイル時に検出することはできません。 - -クエリ実行結果の戻り値の型、接続方法の設定に関しては前章の「データベース接続」にある[Query](/ldbc/ja/04-Database-Connection.html#Query)項目以降を参照してください。 -テーブル定義を使用して構築されたクエリと同じように構築および動作します。 - -プレーンなクエリと型安全なクエリは構築方法が違うだけで後続の接続方法などは同じ実装です。そのため2つを組み合わせてクエリを実行することも可能です。 - -```scala 3 -(for - result1 <- sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)".update - result2 <- userQuery.update("name", "update name").update - ... -yield ...).transaction -``` diff --git a/docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md b/docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md deleted file mode 100644 index 78c697cb5..000000000 --- a/docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md +++ /dev/null @@ -1,76 +0,0 @@ -# SchemaSPYドキュメントの生成 - -この章では、LDBCで構築したテーブル定義を使用して、SchemaSPYドキュメントの作成を行うための方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-schemaspy" % "$version$" -``` -@@@ - -LDBCでのテーブル定義方法をまだ読んでいない場合は、[テーブル定義](/ldbc/ja/01-Table-Definitions.html)の章を先に読むことをオススメしましす。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import ldbc.core.* -import ldbc.schemaspy.SchemaSpyGenerator -``` - -## テーブル定義から生成 - -SchemaSPYはデータベースへ接続を行いMeta情報やテーブル構造を取得しその情報を元にドキュメントを生成しますが、LDBCではデータベースへの接続は行わずLDBCで構築したテーブル構造を使用してSchemaSPYのドキュメントを生成します。 -データベースへの接続を行わないためシンプルにSchemaSPYを使用して生成したドキュメントと乖離する項目があります。例えば、現在テーブルに保存されているレコード数などの情報は表示することができません。 - -ドキュメントを生成するためにはデータベースの情報が必要です。LDBCではデータベースの情報を表現するためのtraitが存在しています。 - -`ldbc.core.Database`を使用してデータベース情報を構築したサンプルは以下になります。 - -```scala 3 -case class SampleLdbcDatabase( - schemaMeta: Option[String] = None, - catalog: Option[String] = Some("def"), - host: String = "127.0.0.1", - port: Int = 3306 -) extends Database: - - override val databaseType: Database.Type = Database.Type.MySQL - - override val name: String = "sample_ldbc" - - override val schema: String = "sample_ldbc" - - override val character: Option[Character] = None - - override val collate: Option[Collate] = None - - override val tables = Set( - ... // LDBCで構築したテーブル構造を列挙 - ) -``` - -SchemaSPYのドキュメント生成には`SchemaSpyGenerator`を使用します。生成したデータベース定義を`default`メソッドに渡し、`generate`を呼び出すと第2引数に指定したファイルの場所にSchemaSPYのファイル群が生成されます。 - -```scala 3 -@main -def run(): Unit = - val file = java.io.File("document") - SchemaSpyGenerator.default(SampleLdbcDatabase(), file).generate() -``` - -生成されたファイルの`index.html`を開くとSchemaSPYのドキュメントを確認することができます。 - -## データベース接続から生成 - -SchemaSpyGeneratorには`connect`メソッドも存在しています。こちらは標準のSchemaSpyの生成方法と同様にデータベースに接続を行いドキュメントの生成を行います。 - -```scala 3 -@main -def run(): Unit = - val file = java.io.File("document") - SchemaSpyGenerator.connect(SampleLdbcDatabase(), "user name", "password" file).generate() -``` - -データベース接続を行う処理はSchemaSpy内部のJavaで書かれた実装で行われます。そのためEffectシステムでスレッドなどが管理されていないことに注意してください。 diff --git a/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md b/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md deleted file mode 100644 index 0a96ff1a1..000000000 --- a/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md +++ /dev/null @@ -1,202 +0,0 @@ -# スキーマコード生成 - -この章では、LDBCのテーブル定義をSQLファイルから自動生成する方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -@@@ vars -```scala 3 -addSbtPlugin("$org$" % "ldbc-plugin" % "$version$") -``` -@@@ - -## 生成 - -プロジェクトに対してプラグインを有効にします。 - -```sbt -lazy val root = (project in file(".")) - .enablePlugins(Ldbc) -``` - -解析対象のSQLファイルを配列で指定します。 - -```sbt -Compile / parseFiles := List(baseDirectory.value / "test.sql") -``` - -**プラグインを有効にすることで設定できるキーの一覧** - -| キー | 詳細 | -|--------------------|------------------------------------------| -| parseFiles | 解析対象のSQLファイルのリスト | -| parseDirectories | 解析対象のSQLファイルをディレクトリ単位で指定する | -| excludeFiles | 解析から除外するファイル名のリスト | -| customYamlFiles | Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト。 | -| classNameFormat | クラス名の書式を指定する値。 | -| propertyNameFormat | Scalaモデルのプロパティ名の形式を指定する値。 | -| ldbcPackage | 生成されるファイルのパッケージ名を指定する値。 | - -解析対象のSQLファイルの先頭には必ずデータベースのCreate文もしくはUse文を定義する必要があります。LDBCはファイルの解析を1ファイルずつ行い、テーブル定義を生成しデータベースモデルにテーブルのリストを格納させます。 -そのためテーブルがどのデータベースに所属しているかを教えてあげる必要があるからです。 - -```sql -CREATE DATABASE `location`; - -USE `location`; - -DROP TABLE IF EXISTS `country`; -CREATE TABLE country ( - `id` BIGINT AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(255) NOT NULL, - `code` INT NOT NULL -); -``` - -解析対象のSQLファイルにはデータベースのCreate/Use文もしくはテーブル定義のCreate/Drop文のみ記載するようにしなければいけません。 - -## 生成コード - -sbtプロジェクトを起動してコンパイルを実行すると、解析対象のSQLファイルを元に生成されたモデルクラスと、テーブル定義がsbtプロジェクトのtarget配下に生成されます。 - -```shell -sbt compile -``` - -上記SQLファイルから生成されるコードは以下のようなものになります。 - -```scala 3 -package ldbc.generated.location - -import ldbc.core.* - -case class Country( - id: Long, - name: String, - code: Int -) - -object Country: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -Compileでコードを生成した場合、その生成されたファイルはキャッシュされるので、SQLファイルを変更していない場合再度生成されることはありません。SQLファイルを変更した場合もしくは、cleanコマンドを実行してキャッシュを削除した場合はCompileを実行すると再度コードが生成されます。 -キャッシュを利用せず再度コード生成を行いたい場合は、`generateBySchema`コマンドを実行してください。このコマンドはキャッシュを使用せず常にコード生成を行います。 - -```shell -sbt generateBySchema -``` - -## カスタマイズ - -SQLファイルから生成されるコードの型を別のものに変換したい時があるかもしれません。その場合は`customYamlFiles`にカスタマイズを行うymlファイルを渡してあげることで行うことができます。 - -```sbt -Compile / customYamlFiles := List( - baseDirectory.value / "custom.yml" -) -``` - -ymlファイルの形式は以下のようなものである必要があります。 - -```yaml -database: - name: '{データベース名}' - tables: - - name: '{テーブル名}' - columns: # Optional - - name: '{カラム名}' - type: '{変更したいScalaの型}' - class: # Optional - extends: - - '{モデルクラスに継承させたいtraitなどのpackageパス}' // package.trait.name - object: # Optional - extends: - - '{オブジェクトに継承させたいtraitなどのpackageパス}' - - name: '{テーブル名}' - ... -``` - -`database`は解析対象のSQLファイルに記載されているデータベース名である必要があります。またテーブル名は解析対象のSQLファイルに記載されているデータベースに所属しているテーブル名である必要があります。 - -`columns`には型を変更したいカラム名と変更したいScalaの型を文字列で記載を行います。`columns`には複数の値を設定できますが、nameに記載されたカラム名が対象のテーブルに含まれいてなければなりません。 -また、変換を行うScalaの型はカラムのData型がサポートしている型である必要があります。もしサポート対象外の型を指定したい場合は、`object`に対して暗黙の型変換を行う設定を持ったtraitやabstract classなどを渡してあげる必要があります。 - -Data型がサポートしている型に関しては[こちら](/ldbc/ja/01-Table-Definitions.html)を、サポート対象外の型を設定する方法は[こちら](/ldbc/ja/02-Custom-Data-Type.html)を参照してください。 - -Int型をユーザー独自の型であるCountryCodeに変換する場合は、以下のような`CustomMapping`traitを実装します。 - -```scala 3 -trait CountryCode: - val code: Int -object Japan extends CountryCode: - override val code: Int = 1 - -trait CustomMapping: // 任意の名前 - given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode] -``` - -カスタマイズを行うためのymlファイルに実装を行なった`CustomMapping`traitを設定し、対象のカラムの型をCountryCodeに変換してあげます。 - -```yaml -database: - name: 'location' - tables: - - name: 'country' - columns: - - name: 'code' - type: 'Country.CountryCode' // CustomMappingをCountryオブジェクトにミックスインさせるのでそこから取得できるように記載 - object: - extends: - - '{package.name.}CustomMapping' -``` - -上記設定で生成されるコードは以下のようになり、ユーザー独自の型でモデルとテーブル定義を生成できるようになります。 - -```scala 3 -case class Country( - id: Long, - name: String, - code: Country.CountryCode -) - -object Country extends /*{package.name.}*/CustomMapping: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -データベースモデルに関してもSQLファイルから自動生成が行われています。 - -```scala 3 -package ldbc.generated.location - -import ldbc.core.* - -case class LocationDatabase( - schemaMeta: Option[String] = None, - catalog: Option[String] = Some("def"), - host: String = "127.0.0.1", - port: Int = 3306 -) extends Database: - - override val databaseType: Database.Type = Database.Type.MySQL - - override val name: String = "location" - - override val schema: String = "location" - - override val character: Option[Character] = None - - override val collate: Option[Collate] = None - - override val tables = Set( - Country.table - ) -``` diff --git a/docs/src/main/mdoc/ja/08-Perdormance.md b/docs/src/main/mdoc/ja/08-Perdormance.md deleted file mode 100644 index af27a61f4..000000000 --- a/docs/src/main/mdoc/ja/08-Perdormance.md +++ /dev/null @@ -1,39 +0,0 @@ -# パフォーマンス - -## コンパイル時間のオーバーヘッド - -テーブル定義のコンパイル時間はカラムの数に応じて増加する - -

Create compile time

- - -クエリ構築のコンパイル時間はselectするカラム数に応じて増加する - -

Create query compile time

- - -## ランタイムのオーバーヘッド - -ldbcは内部的にはTupleを使用しているので、純粋なクラス定義に比べてかなり遅くなってしまう。 - -

Create runtime

- - -ldbcはテーブル定義で他に比べてかなり遅くなってしまう。 - -

Create query runtime

- - -## クエリ実行のオーバーヘッド - -selectクエリの実行は取得するレコード数が増加するにつれてスループットは低くなる - -

Select Throughput

- - -insertクエリの実行は挿入するレコード数が増加するにつれてスループットは低くなる - -※ 実行したクエリが完全に一致するものではないため正確ではない - -

Insert Throughput

- diff --git a/docs/src/main/mdoc/ja/09-Connector.md b/docs/src/main/mdoc/ja/09-Connector.md deleted file mode 100644 index 5fbfee04d..000000000 --- a/docs/src/main/mdoc/ja/09-Connector.md +++ /dev/null @@ -1,842 +0,0 @@ -# コネクタ - -この章では、LDBC独自のMySQLコネクタを使用したデータベース接続について説明します。 - -ScalaでMySQLデータベースへの接続を行うためにはJDBCを使用する必要があります。JDBCはJavaの標準APIであり、Scalaでも使用することができます。 -JDBCはJavaで実装が行われているためScalaで使用する場合でもJVM環境でのみ動作することができます。 - -昨今のScalaを取り巻く環境はJSやNativeなどの環境でも動作できるようプラグインの開発が盛んに行われています。 -ScalaはJavaの資産を使用できるJVMのみで動作する言語から、マルチプラットフォーム環境でも動作できるよう進化を続けています。 - -しかし、JDBCはJavaの標準APIでありScalaのマルチプラットフォーム環境での動作をサポートしていません。 - -そのため、ScalaでアプリケーションをJS, Nativeなどで動作できるように作成を行ったとしてもJDBCを使用できないため、MySQLなどのデータベースへ接続を行うことができません。 - -Typelevel Projectには[Skunk](https://github.com/typelevel/skunk)と呼ばれる[PostgreSQL](https://www.postgresql.org/)用のScalaライブラリが存在します。 -このプロジェクトはJDBCを使用しておらず、純粋なScalaのみでPostgreSQLへの接続を実現しています。そのため、Skunkを使用すればJVM, JS, Native環境を問わずPostgreSQLへの接続を行うことができます。 - -LDBC コネクタはこのSkunkに影響を受けてJVM, JS, Native環境を問わずMySQLへの接続を行えるようにするために開発が行われてるプロジェクトです。 - -※ このコネクタは現在実験的な機能となります。そのため本番環境での使用しないでください。 - -LDBCコネクタは一番低レイヤーのAPIとなります。 -今後このコネクタを使用してより高レイヤーのAPIを提供する予定です。また既存の高レイヤーのAPIとの互換性を持たせることも予定しています。 - -使用するにはプロジェクトに以下の依存関係を設定する必要があります。 - -**JVM** - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" -``` -@@@ - -**JS/Native** - -@@@ vars -```scala -libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" -``` -@@@ - -**サポートバージョン** - -現在のバージョンは以下のバージョンのMySQLをサポートしています。 - -- MySQL 5.7.x -- MySQL 8.x - -メインサポートはMySQL 8.xです。MySQL 5.7.xはサブサポートとなります。そのためMySQL 5.7.xでの動作には注意が必要です。 -将来的にはMySQL 5.7.xのサポートは終了する予定です。 - -## 接続 - -LDBCコネクタを使用してMySQLへの接続を行うためには、`Connection`を使用します。 - -また、`Connection`はオブザーバビリティを意識した開発を行えるように`Otel4s`を使用してテレメトリデータを収集できるようにしています。 -そのため、`Connection`を使用する際には`Otel4s`の`Tracer`を設定する必要があります。 - -開発時やトレースを使用したテレメトリデータが不要な場合は`Tracer.noop`を使用することを推奨します。 - -```scala -import cats.effect.IO -import org.typelevel.otel4s.trace.Tracer -import ldbc.connector.Connection - -given Tracer[IO] = Tracer.noop[IO] - -val connection = Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "root", -) -``` - -以下は`Connection`構築時に設定できるプロパティの一覧です。 - -| プロパティ | 型 | 用途 | -|-------------------------|--------------------|--------------------------------------------------------| -| host | String | MySQLサーバーのホストを指定します | -| port | Int | MySQLサーバーのポート番号を指定します | -| user | String | MySQLサーバーへログインを行うユーザー名を指定します | -| password | Option[String] | MySQLサーバーへログインを行うユーザーのパスワードを指定します | -| database | Option[String] | MySQLサーバーへ接続後に使用するデータベース名を指定します | -| debug | Boolean | 処理のログを出力します。デフォルトはfalseです | -| ssl | SSL | MySQLサーバーとの通知んでSSL/TLSを使用するかを指定します。デフォルトはSSL.Noneです | -| socketOptions | List[SocketOption] | TCP/UDP ソケットのソケットオプションを指定します。 | -| readTimeout | Duration | MySQLサーバーへの接続を試みるまでのタイムアウトを指定します。デフォルトはDuration.Infです。 | -| allowPublicKeyRetrieval | Boolean | MySQLサーバーとの認証時にRSA公開鍵を使用するかを指定します。デフォルトはfalseです。 | - -`Connection`は`Resource`を使用してリソース管理を行います。そのためコネクション情報を使用する場合は`use`メソッドを使用してリソースの管理を行います。 - -```scala -connection.use { conn => - // コードを記述 -} -``` - -### 認証 - -MySQLでの認証は、クライアントがMySQLサーバーへ接続するときにLoginRequestというフェーズでユーザ情報を送信します。そして、サーバー側では送られたユーザが`mysql.user`テーブルに存在するか検索を行い、どの認証プラグインを使用するかを決定します。認証プラグインが決定した後にサーバーはそのプラグインを呼び出してユーザー認証を開始し、その結果をクライアント側に送信します。このようにMySQLでは認証がプラガブル(様々なタイプのプラグインを付け外しできる)になっています。 - -MySQLでサポートされている認証プラグインは[公式ページ](https://dev.mysql.com/doc/refman/8.0/ja/authentication-plugins.html)に記載されています。 - -LDBCは現時点で以下の認証プラグインをサポートしています。 - -- ネイティブプラガブル認証 -- SHA-256 プラガブル認証 -- SHA-2 プラガブル認証のキャッシュ - -※ ネイティブプラガブル認証とSHA-256 プラガブル認証はMySQL 8.xから非推奨となったプラグインです。特段理由がない場合はSHA-2 プラガブル認証のキャッシュを使用することを推奨します。 - -LDBCのアプリケーションコード上で認証プラグインを意識する必要はありません。ユーザーはMySQLのデータベース上で使用したい認証プラグインで作成されたユーザーを作成し、LDBCのアプリケーションコード上ではそのユーザーを使用してMySQLへの接続を試みるだけで問題ありません。 -LDBCが内部で認証プラグインを判断し、適切な認証プラグインを使用してMySQLへの接続を行います。 - -## 実行 - -以降の処理では以下テーブルを使用しているものとします。 - -```sql -CREATE TABLE users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - age INT NULL -); -``` - -### Statement - -`Statement`は動的なパラメーターを使用しないSQLを実行するためのAPIです。 - -※ `Statement`は動的なパラメーターを使用しないため、使い方によってはSQLインジェクションのリスクがあります。そのため、動的なパラメーターを使用する場合は`PreparedStatement`を使用することを推奨します。 - -`Connection`の`createStatement`メソッドを使用して`Statement`を構築します。 - -#### 読み取りクエリ - -読み取り専用のSQLを実行する場合は`executeQuery`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は`ResultSet`に格納されて戻り値として返却されます。 - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeQuery("SELECT * FROM users") - yield - // ResultSetを使用した処理 -} -``` - -#### 書き込みクエリ - -書き込みを行うSQLを実行する場合は`executeUpdate`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は影響を受けた行数が戻り値として返却されます。 - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)") - yield -} -``` - -#### AUTO_INCREMENTの値を取得 - -`Statement`を使用してクエリ実行後にAUTO_INCREMENTの値を取得する場合は`getGeneratedKeys`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値はAUTO_INCREMENTに生成された値が戻り値として返却されます。 - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)", Statement.RETURN_GENERATED_KEYS) - gereatedKeys <- statement.getGeneratedKeys() - yield -} -``` - -### Client/Server PreparedStatement - -LDBCでは`PreparedStatement`を`Client PreparedStatement`と`Server PreparedStatement`に分けて提供しています。 - -`Client PreparedStatement`は動的なパラメーターを使用してアプリケーション上でSQLの構築を行い、MySQLサーバーに送信を行うためのAPIです。 -そのためMySQLサーバーへのクエリ送信方法は`Statement`と同じになります。 - -このAPIはJDBCの`PreparedStatement`に相当します。 - -より安全なMySQLサーバー内でクエリを構築するための`PreparedStatement`は`Server PreparedStatement`で提供されますので、そちらを使用してください。 - -`Server PreparedStatement`は実行を行うクエリをMySQLサーバー内で事前に準備を行い、アプリケーション上でパラメーターを設定して実行を行うためのAPIです。 - -`Server PreparedStatement`では実行するクエリの送信とパラメーターの送信が分けて行われるため、クエリの再利用が可能となります。 - -`Server PreparedStatement`を使用する場合事前にクエリをMySQLサーバーで準備します。格納するためにMySQLサーバーはメモリを使用しますが、クエリの再利用が可能となるため、パフォーマンスの向上が期待できます。 - -しかし、事前準備されたクエリは解放されるまでメモリを使用し続けるため、メモリリークのリスクがあります。 - -`Server PreparedStatement`を使用する場合は`close`メソッドを使用して適切にクエリの解放を行う必要があります。 - -#### Client PreparedStatement - -`Connection`の`clientPreparedStatement`メソッドを使用して`Client PreparedStatement`を構築します。 - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### Server PreparedStatement - -`Connection`の`serverPreparedStatement`メソッドを使用して`Server PreparedStatement`を構築します。 - -```scala -connection.use { conn => - for - statement <- conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### 読み取りクエリ - -読み取り専用のSQLを実行する場合は`executeQuery`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は`ResultSet`に格納されて戻り値として返却されます。 - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - yield - // ResultSetを使用した処理 -} -``` - -動的なパラメーターを使用する場合は`setXXX`メソッドを使用してパラメーターを設定します。 -`setXXX`メソッドは`Option`型を使用することもできます。`None`が渡された場合パラメーターにはNULLがセットされます。 - -`setXXX`メソッドはパラメーターのインデックスとパラメーターの値を指定します。 - -```scala -statement.setLong(1, 1) -``` - -現在のバージョンでは以下のメソッドがサポートされています。 - -| メソッド | 型 | 備考 | -|---------------|-------------------------------------|-----------------------------------| -| setNull | | パラメーターにNULLをセットします | -| setBoolean | Boolean/Option[Boolean] | | -| setByte | Byte/Option[Byte] | | -| setShort | Short/Option[Short] | | -| setInt | Int/Option[Int] | | -| setLong | Long/Option[Long] | | -| setBigInt | BigInt/Option[BigInt] | | -| setFloat | Float/Option[Float] | | -| setDouble | Double/Option[Double] | | -| setBigDecimal | BigDecimal/Option[BigDecimal] | | -| setString | String/Option[String] | | -| setBytes | Array[Byte]/Option[Array[Byte]] | | -| setDate | LocalDate/Option[LocalDate] | `java.sql`ではなく`java.time`を直接扱います。 | -| setTime | LocalTime/Option[LocalTime] | `java.sql`ではなく`java.time`を直接扱います。 | -| setTimestamp | LocalDateTime/Option[LocalDateTime] | `java.sql`ではなく`java.time`を直接扱います。 | -| setYear | Year/Option[Year] | `java.sql`ではなく`java.time`を直接扱います。 | - -#### 書き込みクエリ - -書き込みを行うSQLを実行する場合は`executeUpdate`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は影響を受けた行数が戻り値として返却されます。 - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - yield result -} - -``` - -#### AUTO_INCREMENTの値を取得 - -クエリ実行後にAUTO_INCREMENTの値を取得する場合は`getGeneratedKeys`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値はAUTO_INCREMENTに生成された値が戻り値として返却されます。 - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.executeUpdate() - getGeneratedKeys <- statement.getGeneratedKeys() - yield getGeneratedKeys -} -``` - -### ResultSet - -`ResultSet`はクエリ実行後にMySQLサーバーから返された値を格納するためのAPIです。 - -SQLを実行して取得したレコードを`ResultSet`から取得するにはJDBCと同じように`next`メソッドと`getXXX`メソッドを使用して取得する方法と、LDBC独自の`decode`メソッドを使用する方法があります。 - -#### next/getXXX - -`next`メソッドは次のレコードが存在する場合は`true`を返却し、次のレコードが存在しない場合は`false`を返却します。 - -`getXXX`メソッドはレコードから値を取得するためのAPIです。 - -`getXXX`メソッドは取得するカラムのインデックスを指定する方法とカラム名を指定する方法があります。 - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT `id`, `name`, `age` FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - records <- Monad[IO].whileM(result.next()) { - for - id <- result.getLong(1) - name <- result.getString("name") - age <- result.getInt(3) - yield (id, name, age) - } - yield records -} -``` - -#### decode - -`decode`メソッドは`ResultSet`から取得した値をScalaの型に変換して取得するためのAPIです。 - -取得するカラムの数に応じて`*:`演算子を使用して変換する型を指定します。 - -例では、usersテーブルのid, name, ageカラムを取得する場合を示しておりそれぞれのカラムの型を指定しています。 - -```scala -result.decode(bigint *: varchar *: int.opt) -``` - -NULL許容のカラムを取得する場合は`Option`型に変換するために`opt`メソッドを使用します。 -これによりレコードがNULLの場合はNoneとして取得することができます。 - -クエリ実行からレコード取得までの一連の流れは以下のようになります。 - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - decodes <- result.decode(bigint *: varchar *: int.opt) - yield decodes -} -``` - -`ResultSet`から取得するレコードは常に配列になります。 -これはMySQLで実行するクエリの結果が常に複数のレコードを返す可能性があるからです。 - -単一のレコードを取得する場合は`decode`処理後に、`head`や`headOption`メソッドを使用して取得を行なってください。 - -現在のバージョンでは以下のデータ型がサポートされています。 - -| Codec | データ型 | Scala 型 | -|-------------|-------------------|----------------| -| boolean | BOOLEAN | Boolean | -| tinyint | TINYINT | Byte | -| utinyint | unsigned TINYINT | Short | -| smallint | SMALLINT | Short | -| usmallint | unsigned SMALLINT | Int | -| int | INT | Int | -| uint | unsigned INT | Long | -| bigint | BIGINT | Long | -| ubigint | unsigned BIGINT | BigInt | -| float | FLOAT | Float | -| double | DOUBLE | Double | -| decimal | DECIMAL | BigDecimal | -| char | CHAR | String | -| varchar | VARCHAR | String | -| binary | BINARY | Array[Byte] | -| varbinary | VARBINARY | String | -| tinyblob | TINYBLOB | String | -| blob | BLOB | String | -| mediumblob | MEDIUMBLOB | String | -| longblob | LONGBLOB | String | -| tinytext | TINYTEXT | String | -| text | TEXT | String | -| mediumtext | MEDIUMTEXT | String | -| longtext | LONGTEXT | String | -| enum | ENUM | String | -| set | SET | List[String] | -| json | JSON | String | -| date | DATE | LocalDate | -| time | TIME | LocalTime | -| timetz | TIME | OffsetTime | -| datetime | DATETIME | LocalDateTime | -| timestamp | TIMESTAMP | LocalDateTime | -| timestamptz | TIMESTAMP | OffsetDateTime | -| year | YEAR | Year | - -※ 現在MySQLのデータ型を指定して値を取得するような作りとなっていますが、将来的にはより簡潔にScalaの型を指定して値を取得するような作りに変更する可能性があります。 - -以下サポートされていないデータ型があります。 - -- GEOMETRY -- POINT -- LINESTRING -- POLYGON -- MULTIPOINT -- MULTILINESTRING -- MULTIPOLYGON -- GEOMETRYCOLLECTION - -## トランザクション - -`Connection`を使用してトランザクションを実行するためには`setAutoCommit`メソッドと`commit`メソッド、`rollback`メソッドを組み合わせて使用します。 - -まず、`setAutoCommit`メソッドを使用してトランザクションの自動コミットを無効にします。 - -```scala -conn.setAutoCommit(false) -``` - -何かしらの処理を行った後に`commit`メソッドを使用してトランザクションをコミットします。 - -```scala -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.commit() -yield -``` -もしくは、`rollback`メソッドを使用してトランザクションをロールバックします。 - -```scala -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.rollback() -yield -``` - -`setAutoCommit`メソッドを使用してトランザクションの自動コミットを無効にした場合、コネクションのResourceを解放する際に自動的にロールバックが行われます。 - -### トランザクション分離レベル - -LDBCではトランザクション分離レベルの設定を行うことができます。 - -トランザクション分離レベルは`setTransactionIsolation`メソッドを使用して設定を行います。 - -MySQLでは以下のトランザクション分離レベルがサポートされています。 - -- READ UNCOMMITTED -- READ COMMITTED -- REPEATABLE READ -- SERIALIZABLE - -MySQLのトランザクション分離レベルについては[公式ドキュメント](https://dev.mysql.com/doc/refman/8.0/ja/innodb-transaction-isolation-levels.html)を参照してください。 - -```scala -import ldbc.connector.Connection.TransactionIsolationLevel - -conn.setTransactionIsolation(TransactionIsolationLevel.REPEATABLE_READ) -``` - -現在設定されているトランザクション分離レベルを取得するには`getTransactionIsolation`メソッドを使用します。 - -```scala -for - isolationLevel <- conn.getTransactionIsolation() -yield -``` - -### セーブポイント - -より高度なトランザクション管理のために「Savepoint機能」を使用することができます。これにより、データベース操作中に特定のポイントをマークしておくことが可能になり、何か問題が発生した場合にも、そのポイントまでデータベースの状態を巻き戻すことができます。これは、複雑なデータベース操作や、長いトランザクションの中での安全なポイント設定を必要とする場合に特に役立ちます。 - -**特徴:** - -- 柔軟なトランザクション管理:Savepointを使って、トランザクション内の任意の場所で「チェックポイント」を作成。必要に応じてそのポイントまで状態を戻すことができます。 -- エラー回復:エラーが発生した時、全てを最初からやり直すのではなく、最後の安全なSavepointまで戻ることで、時間の節約と効率の向上が見込めます。 -- 高度な制御:複数のSavepointを設定することで、より精密なトランザクション制御が可能に。開発者はより複雑なロジックやエラーハンドリングを簡単に実装できます。 - -この機能を活用することで、あなたのアプリケーションはより堅牢で信頼性の高いデータベース操作を実現できるようになります。 - -**セーブポイントの設定** - -Savepointを設定するには、`setSavepoint`メソッドを使用します。このメソッドは、Savepointの名前を指定することができます。 -Savepointの名前を指定しない場合、デフォルトの名前としてUUIDで生成された値が設定されます。 - -`getSavepointName`メソッドを使用して、設定されたSavepointの名前を取得することができます。 - -※ MySQLではデフォルトで自動コミットが有効になっているため、Savepointを使用する場合は自動コミットを無効にする必要があります。そうしないと全ての処理が都度コミットされてしまうため、Savepointを使用したトランザクションのロールバックを行うことができなくなるためです。 - -```scala -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") -yield savepoint.getSavepointName -``` - -**セーブポイントのロールバック** - -Savepointを使用してトランザクションの一部をロールバックするには、`rollback`メソッドにSavepointを渡すことでロールバックを行います。 -Savepointを使用して部分的にロールバックをした後、トランザクション全体をコミットするとそのSavepoint以降のトランザクションはコミットされません。 - -```scala -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.rollback(savepoint) - _ <- conn.commit() -yield -``` - -**セーブポイントのリリース** - -Savepointをリリースするには、`releaseSavepoint`メソッドにSavepointを渡すことでリリースを行います。 -Savepointをリリースした後、トランザクション全体をコミットするとそのSavepoint以降のトランザクションはコミットされます。 - -```scala -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.releaseSavepoint(savepoint) - _ <- conn.commit() -yield -``` - -## ユーティリティコマンド - -MySQLにはいくつかのユーティリティコマンドがあります。([参照](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase_utility.html)) - -LDBCではこれらのコマンドを使用するためのAPIを提供しています。 - -| コマンド | 用途 | サポート | -|----------------------|------------------------------------|------| -| COM_QUIT | クライアントが接続を閉じることをサーバーに要求していることを伝える。 | ✅ | -| COM_INIT_DB | 接続のデフォルト・スキーマを変更する | ✅ | -| COM_STATISTICS | 内部ステータスの文字列を可読形式で取得する。 | ✅ | -| COM_DEBUG | サーバーの標準出力にデバッグ情報をダンプする | ❌ | -| COM_PING | サーバーが生きているかチェックする | ✅ | -| COM_CHANGE_USER | 現在の接続のユーザーを変更する | ✅ | -| COM_RESET_CONNECTION | セッションの状態をリセットする | ✅ | -| COM_SET_OPTION | 現在の接続のオプションを設定する | ✅ | - -### COM_QUIT - -`COM_QUIT`はクライアントが接続を閉じることをサーバーに要求していることを伝えるためのコマンドです。 - -LDBCでは`Connection`の`close`メソッドを使用して接続を閉じることができます。 -`close`メソッドを使用すると接続が閉じられるため、その後の処理で接続を使用することはできません。 - -※ `Connection`は`Resource`を使用してリソース管理を行います。そのため`close`メソッドを使用してリソースの解放を行う必要はありません。 - -```scala -connection.use { conn => - conn.close() -} -``` - -### COM_INIT_DB - -`COM_INIT_DB`は接続のデフォルト・スキーマを変更するためのコマンドです。 - -LDBCでは`Connection`の`setSchema`メソッドを使用してデフォルト・スキーマを変更することができます。 - -```scala -connection.use { conn => - conn.setSchema("test") -} -``` - -### COM_STATISTICS - -`COM_STATISTICS`は内部ステータスの文字列を可読形式で取得するためのコマンドです。 - -LDBCでは`Connection`の`getStatistics`メソッドを使用して内部ステータスの文字列を取得することができます。 - -```scala -connection.use { conn => - conn.getStatistics -} -``` - -取得できるステータスは以下のようになります。 - -- `uptime` : サーバーが起動してからの時間 -- `threads` : 現在接続しているクライアントの数 -- `questions` : サーバーが起動してからのクエリの数 -- `slowQueries` : 遅いクエリの数 -- `opens` : サーバーが起動してからのテーブルのオープン数 -- `flushTables` : サーバーが起動してからのテーブルのフラッシュ数 -- `openTables` : 現在オープンしているテーブルの数 -- `queriesPerSecondAvg` : 秒間のクエリの平均数 - -### COM_PING - -`COM_PING`はサーバーが生きているかチェックするためのコマンドです。 - -LDBCでは`Connection`の`isValid`メソッドを使用してサーバーが生きているかチェックすることができます。 -サーバーが生きている場合は`true`を返却し、生きていない場合は`false`を返却します。 - -```scala -connection.use { conn => - conn.isValid -} -``` - -### COM_CHANGE_USER - -`COM_CHANGE_USER`は現在の接続のユーザーを変更するためのコマンドです。 -また、以下の接続状態をリセットします。 - -- ユーザー変数 -- 一時テーブル -- プリペアド・ステートメント -- etc... - -LDBCでは`Connection`の`changeUser`メソッドを使用してユーザーを変更することができます。 - -```scala -connection.use { conn => - conn.changeUser("root", "password") -} -``` - -### COM_RESET_CONNECTION - -`COM_RESET_CONNECTION`はセッションの状態をリセットするためのコマンドです。 - -`COM_RESET_CONNECTION`は`COM_CHANGE_USER`をより軽量化したもので、セッションの状態をクリーンアップする機能はほぼ同じだが、次のような機能がある - -- 再認証を行わない(そのために余分なクライアント/サーバ交換を行わない)。 -- 接続を閉じない。 - -LDBCでは`Connection`の`resetServerState`メソッドを使用してセッションの状態をリセットすることができます。 - -```scala -connection.use { conn => - conn.resetServerState -} -``` - -### COM_SET_OPTION - -`COM_SET_OPTION`は現在の接続のオプションを設定するためのコマンドです。 - -LDBCでは`Connection`の`enableMultiQueries`メソッドと`disableMultiQueries`メソッドを使用してオプションを設定することができます。 - -`enableMultiQueries`メソッドを使用すると、複数のクエリを一度に実行することができます。 -`disableMultiQueries`メソッドを使用すると、複数のクエリを一度に実行することができなくなります。 - -※ これは、Insert、Update、および Delete ステートメントによるバッチ処理にのみ使用できます。Selectステートメントで使用を行なったとしても、最初のクエリの結果のみが返されます。 - -```scala -connection.use { conn => - conn.enableMultiQueries *> conn.disableMultiQueries -} -``` - -## バッチコマンド - -LDBCではバッチコマンドを使用して複数のクエリを一度に実行することができます。 -バッチコマンドを使用することで、複数のクエリを一度に実行することができるため、ネットワークラウンドトリップの回数を減らすことができます。 - -バッチコマンドを使用するには`Statement`または`PreparedStatement`の`addBatch`メソッドを使用してクエリを追加し、`executeBatch`メソッドを使用してクエリを実行します。 - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} -``` - -上記の例では、`Alice`と`Bob`のデータを一度に追加することができます。 -実行されるクエリは以下のようになります。 - -```sql -INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -バッチコマンド実行後の戻り値は、実行したクエリそれぞれの影響を受けた行数の配列となります。 - -上記の例では、`Alice`のデータは1行追加され、`Bob`のデータも1行追加されるため、戻り値は`List(1, 1)`となります。 - -バッチコマンドを実行した後は、今まで`addBatch`メソッドで追加したクエリがクリアされます。 - -手動でクリアする場合は`clearBatch`メソッドを使用してクリアを行います。 - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.clearBatch() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - _ <- statement.executeBatch() - yield -} -``` - -上記の例では、`Alice`のデータは追加されませんが、`Bob`のデータは追加されます。 - -### StatementとPreparedStatementの違い - -`Statement`と`PreparedStatement`ではバッチコマンドで実行されるクエリが異なる場合があります。 - -`Statement`を使用してINSERT文をバッチコマンドで実行した場合、複数のクエリが一度に実行されます。 -しかし、`PreparedStatement`を使用してINSERT文をバッチコマンドで実行した場合、1つのクエリが実行されます。 - -例えば、以下のクエリをバッチコマンドで実行した場合、`Statement`を使用しているため、複数のクエリが一度に実行されます。 - -```scala -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} - -// 実行されるクエリ -// INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -しかし、以下のクエリをバッチコマンドで実行した場合、`PreparedStatement`を使用しているため、1つのクエリが実行されます。 - -```scala -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.addBatch() - _ <- statement.setString(1, "Bob") - _ <- statement.setInt(2, 30) - _ <- statement.addBatch() - result <- statement.executeBatch() - yield result -} - -// 実行されるクエリ -// INSERT INTO users (name, age) VALUES ('Alice', 20), ('Bob', 30); -``` - -これは、`PreparedStatement`を使用している場合、クエリのパラメーターを設定した後に`addBatch`メソッドを使用することで、1つのクエリに複数のパラメーターを設定することができるためです。 - -## ストアドプロシージャの実行 - -LDBCではストアドプロシージャを実行するためのAPIを提供しています。 - -ストアドプロシージャを実行するには`Connection`の`prepareCall`メソッドを使用して`CallableStatement`を構築します。 - -※ 使用するストアドプロシージャは[公式](https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-statements-callable.html)ドキュメント記載のものを使用しています。 - -```sql -CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) -BEGIN - DECLARE z INT; - SET z = inOutParam + 1; - SET inOutParam = z; - - SELECT inputParam; - - SELECT CONCAT('zyxw', inputParam); -END -``` - -上記のストアドプロシージャを実行する場合は以下のようになります。 - -```scala -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - hasResult <- callableStatement.execute() - values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { - for - resultSet <- callableStatement.getResultSet().flatMap { - case Some(rs) => IO.pure(rs) - case None => IO.raiseError(new Exception("No result set")) - } - value <- resultSet.getString(1) - yield value - } - yield values // List(Some("abcdefg"), Some("zyxwabcdefg")) -} -``` - -出力パラメータ(ストアド・プロシージャを作成したときにOUTまたはINOUTとして指定したパラメータ)の値を取得するには、JDBCでは、CallableStatementインターフェイスのさまざまな`registerOutputParameter()`メソッドを使用して、ステートメント実行前にパラメータを指定する必要がありますが、LDBCでは`setXXX`メソッドを使用してパラメータを設定することだけクエリ実行時にパラメーターの設定も行なってくれます。 - -ただし、LDBCでも`registerOutputParameter()`メソッドを使用してパラメータを指定することもできます。 - -```scala -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) - hasResult <- callableStatement.execute() - value <- callableStatement.getInt(2) - yield value // 2 -} -``` - -※ `registerOutParameter`でOutパラメータを指定する場合、同じindex値を使用して`setXXX`メソッドでパラメータを設定していない場合サーバーには`Null`で値が設定されることに注意してください。 - -## 未対応機能 - -LDBCコネクタは現在実験的な機能となります。そのため、以下の機能はサポートされていません。 -機能提供は順次行っていく予定です。 - -- コネクションプーリング -- フェイルオーバー対策 -- etc... diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md deleted file mode 100644 index 3f317d963..000000000 --- a/docs/src/main/mdoc/ja/index.md +++ /dev/null @@ -1,127 +0,0 @@ -@@@ index - * [Table Definitions](./01-Table-Definitions.md) - * [Custom Data Type](./02-Custom-Data-Type.md) - * [Type-safe Query Builder](./03-Type-safe-Query-Builder.md) - * [Database Connection](./04-Database-Connection.md) - * [Plain SQL Queries](./05-Plain-SQL-Queries.md) - * [Generating SchemaSPY Documentation](./06-Generating-SchemaSPY-Documentation.md) - * [Schema Code Generation](./07-Schema-Code-Generation.md) - * [Performance](./08-Perdormance.md) - * [Connector](./09-Connector.md) -@@@ - -# LDBC - -**LDBC**は1.0以前のソフトウェアであり、現在も活発に開発中であることに注意してください。新しいバージョンは以前のバージョンとバイナリ互換性がなくなってしまう可能性があります。 - -## はじめに - -私たちのアプリケーション開発では大抵の場合データベースを使用します。
Scalaでデータベースアクセスを行う場合JDBCを使用する方法がありますが、ScalaにはこのJDBCをラップしたライブラリがいくつか存在しています。 - -- 関数型DSL (Slick, quill, zio-sql) -- SQL文字列インターポレーター (Anorm, doobie) - -LDBCも同じくJDBCをラップしたライブラリであり、LDBCはそれぞれの側面を組み合わせたScala 3ライブラリで、型安全でリファクタブルなSQLインターフェイスを提供し、MySQLのデータベース上でのSQL式を表現できます。 - -また、LDBCのコンセプトは、LDBCを使用することで単一リソースを管理することでScalaのモデルやsqlのスキーマ、ドキュメントを一元化できる開発を行えることです。 - -このコンセプトは宣言的でタイプセーフなWebエンドポイントライブラリである[tapir](https://github.com/softwaremill/tapir)から影響を受けました。
tapirを使用することで、型安全なエンドポイントを構築することができ、構築したエンドポイントからOpenAPIドキュメントを生成することもできます。 - -LDBCはデータベース層でScalaを使用して、同じように型安全な構築を可能にし、構築されたものを使用してドキュメントの生成を行えるようにします。 - -## なぜLDBCなのか? - -データベースを利用したアプリケーション開発では、様々な変更を継続的に行う必要があります。 - -例えば、データベースに構築されたテーブルのどの情報をアプリケーションで扱うべきか、データ検索にはどのようなクエリが最適か、などである。 - -テーブル定義にカラムを1つ追加するだけでも、SQLファイルの修正、対応するモデルへのプロパティの追加、データベースへの反映、ドキュメントの更新などが必要になります。 - -他にも考慮すべきこと、修正すべきことなどたくさんあります。 - -日々の開発の中で全てをメンテナンスし続けるのはとても大変なことであり、メンテナンス漏れだって起こるかもしれません。 - -テーブル情報をアプリケーション・モデルにマッピングすることなく、プレーンなSQLでデータを取得し、データを取得する際には指定された型で取得するというアプローチは非常に良い方法だと思います。 - -この方法であれば、データベース固有のモデルを構築する必要がなく、開発者はデータを取得したいときに、取得したい種類のデータを使って自由にデータを扱うことができるからです。
また、プレーンなクエリを扱うことで、どのようなクエリが実行されるかを瞬時に把握できる点も非常に優れていると思います。 - -しかし、この方法ではテーブル情報のアプケーションでの管理がなくなっただけでドキュメントの更新などを解消することはできません。 - -LDBCは、これらの問題のいくつかを解決するために開発されています。 - -- 型安全性:コンパイル時の保証、開発時の補完、読み取り時の情報 -- 宣言型:テーブル定義の形("What")とデータベース接続("How")を分離する。 -- SchemaSPYの統合:テーブル記述からドキュメントを生成する -- フレームワークではなくライブラリ: あなたのスタックに統合できる - -LDBCを使用するとデータベースの情報をアプリケーションで管理しなければいけませんが、型安全性とクエリの構築、ドキュメントの管理を一元化することができます。 - -LDBCでのモデルをテーブル定義にマッピングするのはとても簡単です。 - -モデルが持つプロパティと、そのカラムのために定義されるデータ型の間のマッピングも非常にシンプルです。開発者は、モデルが持つプロパティと同じ順序で、対応するカラムを定義するだけです。 - -```scala mdoc:silent -import ldbc.core.* - -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) -``` - -また、間違った型を組み合わせようとするとコンパイルエラーになります。 - -例えば、Userが持つString型のnameプロパティに関連するカラムにINT型のカラムを渡すとエラーになります。 - -```shell -[error] -- [E007] Type Mismatch Error: -[error] 169 | column("name", INT), -[error] | ^^^ -[error] |Found: ldbc.core.DataType.Integer[T] -[error] |Required: ldbc.core.DataType[String] -[error] | -[error] |where: T is a type variable with constraint <: Int | Long | Option[Int | Long] -``` - -これらのアドオンの詳細については、[テーブル定義](/ldbc/ja/01-Table-Definitions.html) を参照してください。 - -## クイックスタート - -現在のバージョンは **Scala $scalaVersion$** に対応した **$version$** です。 - -@@@ vars -```scala -libraryDependencies ++= Seq( - - // まずはこの1つから - "$org$" %% "ldbc-core" % "$version$", - - // そして、必要に応じてこれらを加える - "$org$" %% "ldbc-dsl" % "$version$", // プレーンクエリー データベース接続 - "$org$" %% "ldbc-query-builder" % "$version$", // 型安全なクエリ構築 - "$org$" %% "ldbc-schemaspy" % "$version$", // SchemaSPYドキュメント生成 -) -``` -@@@ - -sbtプラグインの使い方については、こちらの[documentation](/ldbc/ja/07-Schema-Code-Generation.html)を参照してください。 - -## TODO - -- JSONデータタイプのサポート -- SETデータタイプのサポート -- Geometryデータタイプのサポート -- CHECK制約のサポート -- MySQL以外のデータベースサポート -- ストリーミングのサポート -- ZIOモジュールのサポート -- 他データベースライブラリとの統合 -- テストキット -- etc... diff --git a/project/plugins.sbt b/project/plugins.sbt index 1425b932d..a63409231 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,10 +2,8 @@ * distributed with this source code. */ -addSbtPlugin("com.github.sbt" % "sbt-site-paradox" % "1.7.0") -addSbtPlugin("com.github.sbt" % "sbt-ghpages" % "0.8.0") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.3") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 9b73c7168ab8723e3a45643d7ae8dadf4f54dfb9 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:24:26 +0900 Subject: [PATCH 048/160] Change docs build settings to sbt-typelevel --- build.sbt | 34 +++++++++++----------------------- project/plugins.sbt | 1 + 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/build.sbt b/build.sbt index 8b9bbe937..ccba39a5a 100644 --- a/build.sbt +++ b/build.sbt @@ -197,33 +197,21 @@ lazy val benchmark = (project in file("benchmark")) lazy val docs = (project in file("docs")) .settings( description := "Documentation for ldbc", - scalacOptions := Nil, - mdocIn := baseDirectory.value / "src" / "main" / "mdoc", - paradoxTheme := Some(builtinParadoxTheme("generic")), - paradoxProperties ++= Map( - "org" -> organization.value, - "scalaVersion" -> scalaVersion.value, - "version" -> version.value.takeWhile(_ != '+'), - "mysqlVersion" -> mysqlVersion - ), - Compile / paradox / sourceDirectory := mdocOut.value, - Compile / paradoxRoots := List("index.html", "en/index.html", "ja/index.html"), - makeSite := makeSite.dependsOn(mdoc.toTask("")).value, - git.remoteRepo := "git@github.com:takapi327/ldbc.git", - ghpagesNoJekyll := true + mdocIn := (Compile / sourceDirectory).value / "mdoc", + Laika / sourceDirectories := Seq((Compile / sourceDirectory).value / "mdoc"), + tlSiteIsTypelevelProject := Some(TypelevelProject.Affiliate), + mdocVariables ++= Map( + "ORGANIZATION" -> organization.value, + "SCALA_VERSION" -> scalaVersion.value, + "MYSQL_VERSION" -> mysqlVersion, + ) ) .settings(commonSettings) .dependsOn( - core.jvm, - sql.jvm, - dsl.jvm, - queryBuilder.jvm, - schema.jvm, - schemaSpy, - codegen.jvm, - hikari + connector.jvm, + schema.jvm ) - .enablePlugins(MdocPlugin, SitePreviewPlugin, ParadoxSitePlugin, GhpagesPlugin, NoPublishPlugin) + .enablePlugins(AutomateHeaderPlugin, TypelevelSitePlugin, NoPublishPlugin) lazy val ldbc = tlCrossRootProject .settings(description := "Pure functional JDBC layer with Cats Effect 3 and Scala 3") diff --git a/project/plugins.sbt b/project/plugins.sbt index a63409231..a4a69bb14 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,6 +2,7 @@ * distributed with this source code. */ +addSbtPlugin("com.github.sbt" % "sbt-ghpages" % "0.8.0") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.1") From 9c556801d35080df3da3df998766412b826a4d2b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:24:57 +0900 Subject: [PATCH 049/160] Create default.template.html for Laika --- docs/src/main/mdoc/default.template.html | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/src/main/mdoc/default.template.html diff --git a/docs/src/main/mdoc/default.template.html b/docs/src/main/mdoc/default.template.html new file mode 100644 index 000000000..1ec929ab9 --- /dev/null +++ b/docs/src/main/mdoc/default.template.html @@ -0,0 +1,50 @@ + + + +@:include(helium.site.templates.head) + + + +@:include(helium.site.templates.topNav) + + + +
+ + @:include(helium.site.templates.pageNav) + +
+ + ${cursor.currentDocument.content} + + @:include(helium.site.templates.footer) + +
+ +
+ + + + From 88258b3fae7c98f2167a191c0f0bcdbb906ea178 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:25:09 +0900 Subject: [PATCH 050/160] Create index.md --- docs/src/main/mdoc/index.md | 237 ++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/src/main/mdoc/index.md diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md new file mode 100644 index 000000000..ff994ba77 --- /dev/null +++ b/docs/src/main/mdoc/index.md @@ -0,0 +1,237 @@ +{% +laika.title = ldbc +laika.metadata { + language = en + isRootPath = true +} +%} + +# ldbc (Lepus Database Connectivity) + +@:image(img/lepus_logo.png) { + alt = "ldbc (Lepus Database Connectivity)" + style = "small-image" +} + +[![Continuous Integration](https://github.com/takapi327/ldbc/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/takapi327/ldbc/actions/workflows/ci.yml) +[![MIT License](https://img.shields.io/badge/license-MIT-green)](https://en.wikipedia.org/wiki/MIT_License) +[![Scala Version](https://img.shields.io/badge/scala-v3.3.x-red)](https://github.com/lampepfl/dotty) +[![Typelevel Affiliate Project](https://img.shields.io/badge/typelevel-affiliate%20project-FF6169.svg)](https://typelevel.org/projects/affiliate/) +[![javadoc](https://javadoc.io/badge2/@ORGANIZATION@/ldbc-dsl_3/javadoc.svg)](https://javadoc.io/doc/@ORGANIZATION@/ldbc-dsl_3) +[![Maven Central Version](https://maven-badges.herokuapp.com/maven-central/@ORGANIZATION@/ldbc-dsl_3/badge.svg?color=blue)](https://search.maven.org/artifact/@ORGANIZATION@/ldbc-dsl_3/0.3.0-beta4/jar) +[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue)](https://index.scala-lang.org/takapi327/ldbc) +[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue&targetType=js)](https://index.scala-lang.org/takapi327/ldbc) +[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue&targetType=native)](https://index.scala-lang.org/takapi327/ldbc) + +======================================================================================== + +ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effect 3 and Scala 3. + +ldbc is a [Typelevel](http://typelevel.org/) project. This means we embrace pure, typeful, functional programming, and provide a safe and friendly environment for teaching, learning, and contributing as described in the Scala [Code of Conduct](http://scala-lang.org/conduct.html). + +Note that **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. + +## Modules availability + +ldbc is available on the JVM, Scala.js, and ScalaNative + +| Module / Platform | JVM | Scala Native | Scala.js | +|----------------------|:---:|:------------:|:--------:| +| `ldbc-core` | ✅ | ✅ | ✅ | +| `ldbc-sql` | ✅ | ✅ | ✅ | +| `ldbc-connector` | ✅ | ✅ | ✅ | +| `jdbc-connector` | ✅ | ❌ | ❌ | +| `ldbc-dsl` | ✅ | ✅ | ✅ | +| `ldbc-query-builder` | ✅ | ✅ | ✅ | +| `ldbc-schema` | ✅ | ✅ | ✅ | +| `ldbc-schemaSpy` | ✅ | ❌ | ❌ | +| `ldbc-codegen` | ✅ | ✅ | ✅ | +| `ldbc-hikari` | ✅ | ❌ | ❌ | +| `ldbc-plugin` | ✅ | ❌ | ❌ | + +## Quick Start + +For people that want to skip the explanations and see it action, this is the place to start! + +### Dependency Configuration + +```scala +libraryDependencies += "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@" +``` + +For Cross-Platform projects (JVM, JS, and/or Native): + +```scala +libraryDependencies += "@ORGANIZATION@" %%% "ldbc-dsl" % "@VERSION@" +``` + +The dependency package used depends on whether the database connection is made via a connector using the Java API or a connector provided by ldbc. + +**Use jdbc connector** + +```scala +libraryDependencies += "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@" +``` + +**Use ldbc connector** + +```scala +libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" +``` + +For Cross-Platform projects (JVM, JS, and/or Native) + +```scala +libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" +``` + +### Usage + +The difference in usage is that there are differences in the way connections are built between jdbc and ldbc. + +> **ldbc** is currently under active development. Please note that current functionality may therefore be deprecated or changed in the future. + +**jdbc connector** + +```scala +val ds = new com.mysql.cj.jdbc.MysqlDataSource() +ds.setServerName("127.0.0.1") +ds.setPortNumber(3306) +ds.setDatabaseName("world") +ds.setUser("ldbc") +ds.setPassword("password") + +val datasource = jdbc.connector.MysqlDataSource[IO](ds) + +val connection: Resource[IO, Connection[IO]] = + Resource.make(datasource.getConnection)(_.close()) +``` + +**ldbc connector** + +```scala +val connection: Resource[IO, Connection[IO]] = + ldbc.connector.Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password"), + database = Some("ldbc"), + ssl = SSL.Trusted + ) +``` + +The connection process to the database can be carried out using the connections established by each of these methods. + +```scala +val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn => + (for + result1 <- sql"SELECT 1".toList[Int] + result2 <- sql"SELECT 2".headOption[Int] + result3 <- sql"SELECT 3".unsafe[Int] + yield (result1, result2, result3)).readOnly(conn) +} +``` + +#### Using the query builder + +ldbc provides not only plain queries but also type-safe database connections using the query builder. + +The first step is to create a schema for use by the query builder. + +ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in definition order. Table definitions are very similar to the structure of Create statements. This makes the construction of table definitions intuitive for the user. + +```scala +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( // CREATE TABLE `user` ( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, + column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL +) +``` + +The next step is to build a TableQuery using the schema you have created. + +```scala +import ldbc.query.builder.TableQuery + +val userQuery = TableQuery[User](table) +``` + +Finally, you can use the query builder to create a query. + +```scala +val result: IO[List[User]] = connection.use { conn => + userQuery.selectAll.toList[User].readOnly(conn) + // "SELECT `id`, `name`, `age` FROM user" +} +``` + +#### Using the schema + +ldbc also allows type-safe construction of schema information for tables. + +The first step is to set up dependencies. + +```scala +libraryDependencies += "@ORGANIZATION@" %% "ldbc-schema" % "@VERSION@" +``` + +For Cross-Platform projects (JVM, JS, and/or Native): + +```scala +libraryDependencies += "@ORGANIZATION@" %%% "ldbc-schema" % "@VERSION@" +``` + +The next step is to create a schema for use by the query builder. + +ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in definition order. Table definitions are very similar to the structure of Create statements. This makes the construction of table definitions intuitive for the user. + +```scala +import ldbc.schema.* + +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val userTable = Table[User]("user")( // CREATE TABLE `user` ( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, + column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL +) // ) +``` + +Finally, you can use the query builder to create a query. + +```scala +val result: IO[List[User]] = connection.use { conn => + userTable.selectAll.query[User].to[List].readOnly(conn) + // "SELECT `id`, `name`, `age` FROM user" +} +``` + +## Documentation + +Full documentation can be found at Currently available in English and Japanese. + +- [English](en/index.md) +- [Japanese](ja/index.md) + +## Contributing + +All suggestions welcome :)! + +If you’d like to contribute, see the list of [issues](https://github.com/takapi327/ldbc/issues) and pick one! Or report your own. If you have an idea you’d like to discuss, that’s always a good option. + +If you have any questions about why or how it works, feel free to ask on github. This probably means that the documentation, scaladocs, and code are unclear and can be improved for the benefit of all. + +### Testing locally + +If you want to build and run the tests for yourself, you'll need a local MySQL database. The easiest way to do this is to run `docker-compose up` from the project root. From 28db3858ca76c2f112f5cf8b8b1790b216c11038 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:25:32 +0900 Subject: [PATCH 051/160] Create img directory --- docs/src/main/mdoc/img/compile_create.png | Bin 0 -> 48533 bytes docs/src/main/mdoc/img/compile_create_query.png | Bin 0 -> 47649 bytes docs/src/main/mdoc/img/insert_throughput.png | Bin 0 -> 53539 bytes docs/src/main/mdoc/img/lepus_logo.png | Bin 0 -> 11636 bytes docs/src/main/mdoc/img/runtime_create.png | Bin 0 -> 42249 bytes docs/src/main/mdoc/img/runtime_create_query.png | Bin 0 -> 50016 bytes docs/src/main/mdoc/img/select_throughput.png | Bin 0 -> 50666 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/src/main/mdoc/img/compile_create.png create mode 100644 docs/src/main/mdoc/img/compile_create_query.png create mode 100644 docs/src/main/mdoc/img/insert_throughput.png create mode 100644 docs/src/main/mdoc/img/lepus_logo.png create mode 100644 docs/src/main/mdoc/img/runtime_create.png create mode 100644 docs/src/main/mdoc/img/runtime_create_query.png create mode 100644 docs/src/main/mdoc/img/select_throughput.png diff --git a/docs/src/main/mdoc/img/compile_create.png b/docs/src/main/mdoc/img/compile_create.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e55433c321db2be1ab12a35ffeded8707cd7a5 GIT binary patch literal 48533 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 literal 0 HcmV?d00001 diff --git a/docs/src/main/mdoc/img/compile_create_query.png b/docs/src/main/mdoc/img/compile_create_query.png new file mode 100644 index 0000000000000000000000000000000000000000..41b7e7809f280df2d9ff1ca1819bad44635ada45 GIT binary patch literal 47649 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

Create compile time

- - -クエリ構築のコンパイル時間はselectするカラム数に応じて増加する - -

Create query compile time

- - -## ランタイムのオーバーヘッド - -ldbcは内部的にはTupleを使用しているので、純粋なクラス定義に比べてかなり遅くなってしまう。 - -

Create runtime

- - -ldbcはテーブル定義で他に比べてかなり遅くなってしまう。 - -

Create query runtime

- - -## クエリ実行のオーバーヘッド - -selectクエリの実行は取得するレコード数が増加するにつれてスループットは低くなる - -

Select Throughput

- - -insertクエリの実行は挿入するレコード数が増加するにつれてスループットは低くなる - -※ 実行したクエリが完全に一致するものではないため正確ではない - -

Insert Throughput

- From 898684481f06d1eb8e968b48918f2f79a405649e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 10 Aug 2024 18:46:09 +0900 Subject: [PATCH 096/160] Replace Schema-Code-Generation document old -> reference --- docs/src/main/mdoc/ja/reference/index.md | 2 +- .../ja/tutorial/Schema-Code-Generation.md} | 39 ++++++++++--------- docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 3 files changed, 24 insertions(+), 20 deletions(-) rename docs/{old/13-Schema-Code-Generation.md => src/main/mdoc/ja/tutorial/Schema-Code-Generation.md} (82%) diff --git a/docs/src/main/mdoc/ja/reference/index.md b/docs/src/main/mdoc/ja/reference/index.md index cc5b27567..cb73b5af7 100644 --- a/docs/src/main/mdoc/ja/reference/index.md +++ b/docs/src/main/mdoc/ja/reference/index.md @@ -7,7 +7,7 @@ このセクションでは、ldbcのコア抽象化と機械の詳細について説明します。 -## Table of Contents +## 目次 @:navigationTree { entries = [ { target = "/ja/reference", depth = 2 } ] diff --git a/docs/old/13-Schema-Code-Generation.md b/docs/src/main/mdoc/ja/tutorial/Schema-Code-Generation.md similarity index 82% rename from docs/old/13-Schema-Code-Generation.md rename to docs/src/main/mdoc/ja/tutorial/Schema-Code-Generation.md index 0a96ff1a1..1f0b57850 100644 --- a/docs/old/13-Schema-Code-Generation.md +++ b/docs/src/main/mdoc/ja/tutorial/Schema-Code-Generation.md @@ -1,14 +1,17 @@ +{% + laika.title = スキーマコード生成 + laika.metadata.language = ja +%} + # スキーマコード生成 -この章では、LDBCのテーブル定義をSQLファイルから自動生成する方法について説明します。 +この章では、ldbcのテーブル定義をSQLファイルから自動生成する方法について説明します。 プロジェクトに以下の依存関係を設定する必要があります。 -@@@ vars ```scala 3 -addSbtPlugin("$org$" % "ldbc-plugin" % "$version$") +addSbtPlugin("@ORGANIZATION@" % "ldbc-plugin" % "@VERSION@") ``` -@@@ ## 生成 @@ -27,17 +30,17 @@ Compile / parseFiles := List(baseDirectory.value / "test.sql") **プラグインを有効にすることで設定できるキーの一覧** -| キー | 詳細 | -|--------------------|------------------------------------------| -| parseFiles | 解析対象のSQLファイルのリスト | -| parseDirectories | 解析対象のSQLファイルをディレクトリ単位で指定する | -| excludeFiles | 解析から除外するファイル名のリスト | -| customYamlFiles | Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト。 | -| classNameFormat | クラス名の書式を指定する値。 | -| propertyNameFormat | Scalaモデルのプロパティ名の形式を指定する値。 | -| ldbcPackage | 生成されるファイルのパッケージ名を指定する値。 | - -解析対象のSQLファイルの先頭には必ずデータベースのCreate文もしくはUse文を定義する必要があります。LDBCはファイルの解析を1ファイルずつ行い、テーブル定義を生成しデータベースモデルにテーブルのリストを格納させます。 +| キー | 詳細 | +|----------------------|-------------------------------------------| +| `parseFiles` | `解析対象のSQLファイルのリスト` | +| `parseDirectories` | `解析対象のSQLファイルをディレクトリ単位で指定する` | +| `excludeFiles` | `解析から除外するファイル名のリスト` | +| `customYamlFiles` | `Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト` | +| `classNameFormat` | `クラス名の書式を指定する値` | +| `propertyNameFormat` | `Scalaモデルのプロパティ名の形式を指定する値` | +| `ldbcPackage` | `生成されるファイルのパッケージ名を指定する値` | + +解析対象のSQLファイルの先頭には必ずデータベースのCreate文もしくはUse文を定義する必要があります。ldbcはファイルの解析を1ファイルずつ行い、テーブル定義を生成しデータベースモデルにテーブルのリストを格納させます。 そのためテーブルがどのデータベースに所属しているかを教えてあげる必要があるからです。 ```sql @@ -68,7 +71,7 @@ sbt compile ```scala 3 package ldbc.generated.location -import ldbc.core.* +import ldbc.schema.* case class Country( id: Long, @@ -126,7 +129,7 @@ database: `columns`には型を変更したいカラム名と変更したいScalaの型を文字列で記載を行います。`columns`には複数の値を設定できますが、nameに記載されたカラム名が対象のテーブルに含まれいてなければなりません。 また、変換を行うScalaの型はカラムのData型がサポートしている型である必要があります。もしサポート対象外の型を指定したい場合は、`object`に対して暗黙の型変換を行う設定を持ったtraitやabstract classなどを渡してあげる必要があります。 -Data型がサポートしている型に関しては[こちら](/ldbc/ja/01-Table-Definitions.html)を、サポート対象外の型を設定する方法は[こちら](/ldbc/ja/02-Custom-Data-Type.html)を参照してください。 +Data型がサポートしている型に関しては[こちら](/ja/tutorial/Schema.md#データ型)を、サポート対象外の型を設定する方法は[こちら](/ja/tutorial/Schema.md#カスタム-データ型)を参照してください。 Int型をユーザー独自の型であるCountryCodeに変換する場合は、以下のような`CustomMapping`traitを実装します。 @@ -177,7 +180,7 @@ object Country extends /*{package.name.}*/CustomMapping: ```scala 3 package ldbc.generated.location -import ldbc.core.* +import ldbc.schema.* case class LocationDatabase( schemaMeta: Option[String] = None, diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 81cb1cb1c..a95cc9a92 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -12,5 +12,6 @@ laika.navigationOrder = [ Logging.md, Custom-Data-Type.md, Query-Builder.md, - Schema.md + Schema.md, + Schema-Code-Generation.md ] From 48409bbb19799ccb50a344d7f491180b51c35da8 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 10 Aug 2024 18:48:09 +0900 Subject: [PATCH 097/160] Fixed Connector document --- docs/src/main/mdoc/ja/reference/Connector.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/src/main/mdoc/ja/reference/Connector.md b/docs/src/main/mdoc/ja/reference/Connector.md index 0cd48fdd9..e2ae6d8cf 100644 --- a/docs/src/main/mdoc/ja/reference/Connector.md +++ b/docs/src/main/mdoc/ja/reference/Connector.md @@ -31,19 +31,15 @@ ldbcコネクタは一番低レイヤーのAPIとなります。 **JVM** -@@@ vars ```scala 3 -libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" +libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" ``` -@@@ **JS/Native** -@@@ vars ```scala 3 -libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" +libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" ``` -@@@ **サポートバージョン** From 2e529602f427df4c57d241bc3b60710145491c8a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 11 Aug 2024 00:34:02 +0900 Subject: [PATCH 098/160] Fixed Connector document --- docs/src/main/mdoc/ja/reference/Connector.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/main/mdoc/ja/reference/Connector.md b/docs/src/main/mdoc/ja/reference/Connector.md index e2ae6d8cf..430273981 100644 --- a/docs/src/main/mdoc/ja/reference/Connector.md +++ b/docs/src/main/mdoc/ja/reference/Connector.md @@ -564,7 +564,7 @@ ldbcではこれらのコマンドを使用するためのAPIを提供してい | `COM_RESET_CONNECTION` | `セッションの状態をリセットする` | ✅ | | `COM_SET_OPTION` | `現在の接続のオプションを設定する` | ✅ | -### COM_QUIT +### COM QUIT `COM_QUIT`はクライアントが接続を閉じることをサーバーに要求していることを伝えるためのコマンドです。 @@ -579,7 +579,7 @@ connection.use { conn => } ``` -### COM_INIT_DB +### COM INIT DB `COM_INIT_DB`は接続のデフォルト・スキーマを変更するためのコマンドです。 @@ -591,7 +591,7 @@ connection.use { conn => } ``` -### COM_STATISTICS +### COM STATISTICS `COM_STATISTICS`は内部ステータスの文字列を可読形式で取得するためのコマンドです。 @@ -614,7 +614,7 @@ connection.use { conn => - `openTables` : 現在オープンしているテーブルの数 - `queriesPerSecondAvg` : 秒間のクエリの平均数 -### COM_PING +### COM PING `COM_PING`はサーバーが生きているかチェックするためのコマンドです。 @@ -627,7 +627,7 @@ connection.use { conn => } ``` -### COM_CHANGE_USER +### COM CHANGE USER `COM_CHANGE_USER`は現在の接続のユーザーを変更するためのコマンドです。 また、以下の接続状態をリセットします。 @@ -645,7 +645,7 @@ connection.use { conn => } ``` -### COM_RESET_CONNECTION +### COM RESET CONNECTION `COM_RESET_CONNECTION`はセッションの状態をリセットするためのコマンドです。 @@ -662,7 +662,7 @@ connection.use { conn => } ``` -### COM_SET_OPTION +### COM SET OPTION `COM_SET_OPTION`は現在の接続のオプションを設定するためのコマンドです。 From d74d16db7ad55fde53ce248cfee4e0595d64f717 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 19 Aug 2024 22:42:58 +0900 Subject: [PATCH 099/160] Update plugin version --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index df9d71625..ffaba7965 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.2") -addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.1") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("com.armanbilge" % "sbt-scala-native-config-brew-github-actions" % "0.3.0") From e92d54c357d1d6c0a414a1850575e1ffe8980a8b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 19 Aug 2024 22:44:38 +0900 Subject: [PATCH 100/160] Action sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29962fabb..f97fb3b8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,10 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: + - name: Install sbt + if: contains(runner.os, 'macos') + run: brew install sbt + - name: Checkout current branch (full) uses: actions/checkout@v4 with: @@ -133,6 +137,10 @@ jobs: java: [corretto@11] runs-on: ${{ matrix.os }} steps: + - name: Install sbt + if: contains(runner.os, 'macos') + run: brew install sbt + - name: Checkout current branch (full) uses: actions/checkout@v4 with: @@ -257,6 +265,10 @@ jobs: java: [corretto@11] runs-on: ${{ matrix.os }} steps: + - name: Install sbt + if: contains(runner.os, 'macos') + run: brew install sbt + - name: Checkout current branch (full) uses: actions/checkout@v4 with: @@ -327,6 +339,10 @@ jobs: java: [corretto@11] runs-on: ${{ matrix.os }} steps: + - name: Install sbt + if: contains(runner.os, 'macos') + run: brew install sbt + - name: Checkout current branch (full) uses: actions/checkout@v4 with: @@ -378,6 +394,10 @@ jobs: java: [temurin@11, temurin@17] runs-on: ${{ matrix.os }} steps: + - name: Install sbt + if: contains(runner.os, 'macos') + run: brew install sbt + - name: Checkout current branch (full) uses: actions/checkout@v4 with: From e67f926ea0bd8b1974bb4eb62fa5d863b2e76302 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 23 Aug 2024 21:48:42 +0900 Subject: [PATCH 101/160] Fixed compile --- module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala index 390b5a9ec..d900c9a0a 100644 --- a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala +++ b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala @@ -195,7 +195,6 @@ package object dsl: export ldbc.dsl.logging.LogHandler export ldbc.dsl.Parameter - type ResultSetReaderIO[T] = ldbc.dsl.ResultSetReader[F, T] export ldbc.dsl.ResultSetReader type PreparedStatementIO = ldbc.sql.PreparedStatement[F] From 70331e1acb358a662709b48076682328ad98af36 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 23 Aug 2024 21:55:05 +0900 Subject: [PATCH 102/160] Fixed compile --- docs/src/main/scala/05-Program.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index 299210e20..1a927a35e 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -42,8 +42,8 @@ import ldbc.dsl.io.* // #program1 // #customReader - given ResultSetReader[IO, Status] = - ResultSetReader.mapping[IO, Boolean, Status] { + given ResultSetReader[Status] = + ResultSetReader.mapping[Boolean, Status] { case true => Status.Active case false => Status.InActive } From a7bf6f9692f8f027697ed9e53838f7127a190401 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 23 Aug 2024 22:22:29 +0900 Subject: [PATCH 103/160] Delete unused --- docs/src/main/scala/05-Program.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index 1a927a35e..dbc736c0a 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -6,8 +6,6 @@ import scala.language.implicitConversions -import cats.syntax.all.* - import cats.effect.* import cats.effect.unsafe.implicits.global From 54b8f732c3c4f3acc3fd1a5af32bb52aa3ea7712 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 28 Sep 2024 20:09:11 +0900 Subject: [PATCH 104/160] Fixed compile --- docs/src/main/scala/00-Setup.scala | 2 +- docs/src/main/scala/05-Program.scala | 35 +++++-------------- .../src/main/scala/ldbc/dsl/package.scala | 6 ---- 3 files changed, 10 insertions(+), 33 deletions(-) diff --git a/docs/src/main/scala/00-Setup.scala b/docs/src/main/scala/00-Setup.scala index ffd806360..b1474cfa7 100644 --- a/docs/src/main/scala/00-Setup.scala +++ b/docs/src/main/scala/00-Setup.scala @@ -124,7 +124,7 @@ import ldbc.dsl.io.* connection .use { conn => createDatabase.commit(conn) *> - conn.setSchema("sandbox_db") *> + conn.setCatalog("sandbox_db") *> (setUpTables *> insertData) .transaction(conn) .as(println("Database setup completed")) diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index dbc736c0a..1dcf42be9 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -13,46 +13,32 @@ import org.typelevel.otel4s.trace.Tracer import ldbc.connector.* import ldbc.dsl.io.* +import ldbc.dsl.codec.* @main def program5(): Unit = - // #given given Tracer[IO] = Tracer.noop[IO] given LogHandler[IO] = LogHandler.noop[IO] - // #given - // #customType enum Status: case Active, InActive - // #customType - // #customParameter - given Parameter[Status] with - override def bind[F[_]](statement: PreparedStatement[F], index: Int, status: Status): F[Unit] = - status match - case Status.Active => statement.setBoolean(index, true) - case Status.InActive => statement.setBoolean(index, false) - // #customParameter + given Encoder[Status] with + override def encode(value: Status): Boolean = value match + case Status.Active => true + case Status.InActive => false - // #program1 val program1: Executor[IO, Int] = sql"INSERT INTO user (name, email, status) VALUES (${ "user 1" }, ${ "user@example.com" }, ${ Status.Active })".update - // #program1 - // #customReader - given ResultSetReader[Status] = - ResultSetReader.mapping[Boolean, Status] { - case true => Status.Active - case false => Status.InActive - } - // #customReader + given Decoder.Elem[Status] = Decoder.Elem.mapping[Boolean, Status] { + case true => Status.Active + case false => Status.InActive + } - // #program2 val program2: Executor[IO, (String, String, Status)] = sql"SELECT name, email, status FROM user WHERE id = 1".query[(String, String, Status)].unsafe - // #program2 - // #connection def connection = Connection[IO]( host = "127.0.0.1", port = 13306, @@ -60,12 +46,9 @@ import ldbc.dsl.io.* password = Some("password"), ssl = SSL.Trusted ) - // #connection - // #run connection .use { conn => program1.commit(conn) *> program2.readOnly(conn).map(println(_)) } .unsafeRunSync() - // #run diff --git a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala index 2dcc04c5b..2bcf63766 100644 --- a/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala +++ b/module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala @@ -194,12 +194,6 @@ package object dsl: export ldbc.dsl.Executor export ldbc.dsl.logging.LogHandler - export ldbc.dsl.Parameter - - export ldbc.dsl.ResultSetReader - - type PreparedStatementIO = ldbc.sql.PreparedStatement[F] - export ldbc.sql.PreparedStatement implicit def logger: LogHandler[F] = LogHandler.noop[F] From 8362c03812035e6b0060e5f73288b64e0a691532 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 28 Sep 2024 20:15:55 +0900 Subject: [PATCH 105/160] Update Migration Notes document --- docs/src/main/mdoc/Migration-Notes.md | 95 ++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/docs/src/main/mdoc/Migration-Notes.md b/docs/src/main/mdoc/Migration-Notes.md index 81041c51f..6cbf5901d 100644 --- a/docs/src/main/mdoc/Migration-Notes.md +++ b/docs/src/main/mdoc/Migration-Notes.md @@ -234,7 +234,100 @@ val userTable = Table[User] ```scala val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query[User].to[List].readOnly(conn) + userTable.selectAll.query.to[List].readOnly(conn) // "SELECT `id`, `name`, `age` FROM user" } ``` + +#### カスタムデータ型のサポート + +ユーザー定義のデータ型を使用する際は、`ResultSetReader`と`Parameter`を使用してカスタムデータ型をサポートしていました。 + +今回の更新で、`ResultSetReader`と`Parameter`を使用してカスタムデータ型をサポートする方法が変更されました。 + +##### Encoder + +クエリ文字列に動的に埋め込むために、`Parameter`から`Encoder`に変更。 + +これにより、ユーザはEffect Typeを受け取るための冗長な処理を記述する必要がなくなり、よりシンプルな実装とカスタムデータ型のパラメータとしての使用が可能になります。 + +```scala +enum Status(val code: Int, val name: String): + case Active extends Status(1, "Active") + case InActive extends Status(2, "InActive") +``` + +**Before** + +```scala +given Parameter[Status] with + override def bind[F[_]]( + statement: PreparedStatement[F], + index: Int, + status: Status + ): F[Unit] = statement.setInt(index, status.code) +``` + +**After** + +```scala +given Encoder[Status] with + override def encode(status: Status): Int = status.done +``` + +`Encoder`のエンコード処理では、`PreparedStatement`で扱えるScala型しか返すことができません。 + +現在、以下のタイプがサポートされている。 + +| Scala Type | Methods called in PreparedStatement | +|-------------------------|-------------------------------------| +| Boolean | setBoolean | +| Byte | setByte | +| Short | setShort | +| Int | setInt | +| Long | setLong | +| Float | setFloat | +| Double | setDouble | +| BigDecimal | setBigDecimal | +| String | setString | +| Array[Byte] | setBytes | +| java.time.LocalDate | setDate | +| java.time.LocalTime | setTime | +| java.time.LocalDateTime | setTimestamp | +| None | setNull | + +##### Decoder + +`ResultSet`からデータを取得する処理を`ResultSetReader`から`Decoder`に変更。 + +これにより、ユーザーは取得したレコードをネストした階層データに変換できる。 + + +```scala +case class City(id: Int, name: String, countryCode: String) +case class Country(code: String, name: String) +case class CityWithCountry(city: City, country: Country) + +sql"SELECT city.Id, city.Name, city.CountryCode, country.Code, country.Name FROM city JOIN country ON city.CountryCode = country.Code".query[CityWithCountry] +``` + +**Using Query Builder** + +```scala +case class City(id: Int, name: String, countryCode: String) derives Table +case class Country(code: String, name: String) derives Table + +val city = Table[City] +val country = Table[Country] + +city.join(country).join((city, country) => city.countryCode === country.code) + .select((city, country) => (city.name, country.name)) + .query // (String, String) + .to[Option] + + +city.join(country).join((city, country) => city.countryCode === country.code) + .selectAll + .query // (City, Country) + .to[Option] +``` From fd644895f6d204c5ee59b188aaa68d83c6345c45 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 28 Sep 2024 21:53:56 +0900 Subject: [PATCH 106/160] Update README AND top index.md --- README.md | 4 ++-- docs/src/main/mdoc/index.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e561338be..6f74f7062 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Finally, you can use the query builder to create a query. ```scala val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query[User].to[List].readOnly(conn) + userTable.selectAll.query.to[List].readOnly(conn) // "SELECT `id`, `name`, `age` FROM user" } ``` @@ -213,7 +213,7 @@ Finally, you can use the query builder to create a query. ```scala val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query[User].to[List].readOnly(conn) + userTable.selectAll.query.to[List].readOnly(conn) // "SELECT `id`, `name`, `age` FROM user" } ``` diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md index c1a4c37bc..1563832be 100644 --- a/docs/src/main/mdoc/index.md +++ b/docs/src/main/mdoc/index.md @@ -173,7 +173,7 @@ Finally, you can use the query builder to create a query. ```scala val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query[User].to[List].readOnly(conn) + userTable.selectAll.query.to[List].readOnly(conn) // "SELECT `id`, `name`, `age` FROM user" } ``` @@ -218,7 +218,7 @@ Finally, you can use the query builder to create a query. ```scala val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query[User].to[List].readOnly(conn) + userTable.selectAll.query.to[List].readOnly(conn) // "SELECT `id`, `name`, `age` FROM user" } ``` From 812c8c8f9776babd65ffac1cc96bc68d121f9a73 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 28 Sep 2024 21:54:30 +0900 Subject: [PATCH 107/160] Update CONTRIBUTING document --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a40e67e4..62bcffa33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ These guidelines are meant to be a living document that should be changed and ad ## Tooling -LDBC is built with [sbt](https://github.com/sbt/sbt), and you should be able to jump right in by running `sbt test`. +ldbc is built with [sbt](https://github.com/sbt/sbt), and you should be able to jump right in by running `sbt test`. Please make sure to run `sbt scalafmtAll` (and commit the results!) before opening a pull request. This will take care of running both scalafmt, ensuring that the build doesn't just immediately fail to compile your work. From 6a18a9cfb9309a6789ea6c7bc732b6eba0c497c9 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 28 Sep 2024 21:54:49 +0900 Subject: [PATCH 108/160] Update Migration Notes --- docs/src/main/mdoc/Migration-Notes.md | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/src/main/mdoc/Migration-Notes.md b/docs/src/main/mdoc/Migration-Notes.md index 6cbf5901d..0b8a0c1c2 100644 --- a/docs/src/main/mdoc/Migration-Notes.md +++ b/docs/src/main/mdoc/Migration-Notes.md @@ -279,22 +279,22 @@ given Encoder[Status] with 現在、以下のタイプがサポートされている。 -| Scala Type | Methods called in PreparedStatement | -|-------------------------|-------------------------------------| -| Boolean | setBoolean | -| Byte | setByte | -| Short | setShort | -| Int | setInt | -| Long | setLong | -| Float | setFloat | -| Double | setDouble | -| BigDecimal | setBigDecimal | -| String | setString | -| Array[Byte] | setBytes | -| java.time.LocalDate | setDate | -| java.time.LocalTime | setTime | -| java.time.LocalDateTime | setTimestamp | -| None | setNull | +| Scala Type | Methods called in PreparedStatement | +|---------------------------|-------------------------------------| +| `Boolean` | `setBoolean` | +| `Byte` | `setByte` | +| `Short` | `setShort` | +| `Int` | `setInt` | +| `Long` | `setLong` | +| `Float` | `setFloat` | +| `Double` | `setDouble` | +| `BigDecimal` | `setBigDecimal` | +| `String` | `setString` | +| `Array[Byte]` | `setBytes` | +| `java.time.LocalDate` | `setDate` | +| `java.time.LocalTime` | `setTime` | +| `java.time.LocalDateTime` | `setTimestamp` | +| `None` | `setNull` | ##### Decoder From 7dcf6d39031aa7cdec94989f6fec18fae85f416e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 28 Sep 2024 21:55:00 +0900 Subject: [PATCH 109/160] Update document --- docs/src/main/mdoc/ja/index.md | 15 ++++--- .../main/mdoc/ja/tutorial/Custom-Data-Type.md | 40 ++++++++----------- .../main/mdoc/ja/tutorial/Selecting-Data.md | 2 +- .../main/mdoc/ja/tutorial/Simple-Program.md | 4 +- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index 898ecf6a1..0251f4191 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -13,26 +13,29 @@ ldbcは[Typelevel](http://typelevel.org/)プロジェクトです。これは、 ## はじめに -私たちのアプリケーション開発では大抵の場合データベースを使用します。
Scalaでデータベースアクセスを行う場合JDBCを使用する方法がありますが、ScalaにはこのJDBCをラップしたライブラリがいくつか存在しています。 +私たちのアプリケーション開発では大抵の場合データベースを使用します。 + +Scalaでデータベースアクセスを行う場合JDBCを使用する方法がありますが、ScalaにはこのJDBCをラップしたライブラリがいくつか存在しています。 - 関数型DSL (Slick, quill, zio-sql) - SQL文字列インターポレーター (Anorm, doobie) -ldbcも同じくJDBCをラップしたライブラリであり、ldbcはそれぞれの側面を組み合わせたScala 3ライブラリで、型安全でリファクタブルなSQLインターフェイスを提供し、MySQLのデータベース上でのSQL式を表現できます。 +ldbcも同じくJDBCをラップしたライブラリであり、ldbcはそれぞれの側面を組み合わせたScala 3ライブラリで型安全でリファクタブルなSQLインターフェイスを提供し、MySQLのデータベース上でのSQL式を表現できます。 ldbcは他のライブラリとは異なり、Scalaで構築された独自のコネクターも提供しています。 Scalaは現在JVM, JS, Nativeというマルチプラットフォームに対応しています。 -しかし、JDBCを使用したライブラリだとJVM環境でしか動作しません。
-そのためldbcは、MySQLプロトコルに対応したScalaで書かれたコネクタを提供することで、異なるプラットフォームで動作できるようにするために開発を行っています。
+しかし、JDBCを使用したライブラリだとJVM環境でしか動作しません。 + +そのためldbcは、MySQLプロトコルに対応したScalaで書かれたコネクタを提供することで異なるプラットフォームで動作できるようにするために開発を行っています。 ldbcを使用することで、Scalaの型安全性と関数型プログラミングの利点を活かしながら、プラットフォームを問わずにデータベースアクセスを行うことができます。 また、ldbcを使用することで単一リソースを管理することでScalaのモデルやsqlのスキーマ、ドキュメントを一元化できる開発を行えることです。 -このコンセプトは宣言的でタイプセーフなWebエンドポイントライブラリである[tapir](https://github.com/softwaremill/tapir)から影響を受けました。
tapirを使用することで型安全なエンドポイントを構築することができ、構築したエンドポイントからOpenAPIドキュメントを生成することもできます。 +このコンセプトは宣言的でタイプセーフなWebエンドポイントライブラリである[tapir](https://github.com/softwaremill/tapir)から影響を受けました。tapirを使用することで型安全なエンドポイントを構築することができ、構築したエンドポイントからOpenAPIドキュメントを生成することもできます。 -ldbcはデータベース層でScalaを使用して、同じように型安全な構築を可能にし、構築されたものを使用してドキュメントの生成を行えるようにします。 +ldbcはデータベース層でScalaを使用して、同じように型安全な構築を可能にし構築されたものを使用してドキュメントの生成を行えるようにします。 ### 対象読者 diff --git a/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md b/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md index 77ca47563..21e0400ca 100644 --- a/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md +++ b/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md @@ -13,34 +13,27 @@ ALTER TABLE user ADD COLUMN status BOOLEAN NOT NULL DEFAULT TRUE; ``` -## 受け渡し +## Encoder -ldbcではstatementに受け渡す値を`Parameter`で表現しています。`Parameter`はstatementへのバインドする値を表現するためのtraitです。 +ldbcではstatementに受け渡す値を`Encoder`で表現しています。`Encoder`はstatementへのバインドする値を表現するためのtraitです。 -`Parameter`を実装することでstatementに受け渡す値をカスタム型で表現することができます。 +`Encoder`を実装することでstatementに受け渡す値をカスタム型で表現することができます。 ユーザー情報にそのユーザーのステータスを表す`Status`を追加します。 ```scala 3 enum Status(val done: Boolean, val name: String): - case Active extends TaskStatus(false, "Active") - case InActive extends TaskStatus(true, "InActive") + case Active extends Status(false, "Active") + case InActive extends Status(true, "InActive") ``` -以下のコード例では、`Parameter`を実装した`CustomParameter`を定義しています。 +以下のコード例では、カスタム型の`Encoder`を定義しています。 これによりstatementにカスタム型をバインドすることができるようになります。 ```scala 3 -given Parameter[Status] with - override def bind[F[_]]( - statement: PreparedStatement[F], - index: Int, - status: Status - ): F[Unit] = - status match - case Status.Active => statement.setBoolean(index, true) - case Status.InActive => statement.setBoolean(index, false) +given Encoder[Status] with + override def encode(status: Status): Boolean = status.done ``` カスタム型は他のパラメーターと同じようにstatementにバインドすることができます。 @@ -52,20 +45,19 @@ val program1: Executor[IO, Int] = これでstatementにカスタム型をバインドすることができるようになりました。 -## 読み取り +## Decoder -ldbcではパラメーターの他に実行結果から独自の型を取得するための`ResultSetReader`も提供しています。 +ldbcではパラメーターの他に実行結果から独自の型を取得するための`Decoder`も提供しています。 -`ResultSetReader`を実装することでstatementの実行結果から独自の型を取得することができます。 +`Decoder`を実装することでstatementの実行結果から独自の型を取得することができます。 -以下のコード例では、`ResultSetReader`を実装した`CustomResultSetReader`を定義しています。 +以下のコード例では、`Decoder.Elem`を使用して単一のデータ型を取得する方法を示しています。 ```scala 3 -given ResultSetReader[IO, Status] = - ResultSetReader.mapping[IO, Boolean, Status] { - case true => Status.Active - case false => Status.InActive - } + given Decoder.Elem[Status] = Decoder.Elem.mapping[Boolean, Status] { + case true => Status.Active + case false => Status.InActive +} ``` ```scala 3 diff --git a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md index 34dc4c504..a2a2359cd 100644 --- a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md +++ b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md @@ -44,7 +44,7 @@ sql"SELECT name, email FROM user" ldbcは、複数のカラムを選択してクラスにマッピングすることもできます。 ```scala -case class User(name: String, population: Int) +case class User(name: String, email: String) sql"SELECT name, email FROM user" .query[User] // Query[IO, User] diff --git a/docs/src/main/mdoc/ja/tutorial/Simple-Program.md b/docs/src/main/mdoc/ja/tutorial/Simple-Program.md index 0896f67f4..2cc7d8517 100644 --- a/docs/src/main/mdoc/ja/tutorial/Simple-Program.md +++ b/docs/src/main/mdoc/ja/tutorial/Simple-Program.md @@ -13,13 +13,13 @@ このプログラムでは、データベースに接続し計算結果を取得するプログラムを作成します。 -それでは、sql string interpolatorを使って、データベースに定数の計算を依頼する問い合わせを作成してみましょう。 +それでは、`sql string interpolator`を使って、データベースに定数の計算を依頼する問い合わせを作成してみましょう。 ```scala val program: Executor[IO, Option[Int]] = sql"SELECT 2".query[Int].to[Option] ``` -sql string interpolatorを使って作成したクエリは`query`メソッドで取得する型の決定を行います。ここでは`Int`型を取得するため、`query[Int]`としています。また、`to`メソッドで取得する型を決定します。ここでは`Option`型を取得するため、`to[Option]`としています。 +`sql string interpolator`を使って作成したクエリは`query`メソッドで取得する型の決定を行います。ここでは`Int`型を取得するため、`query[Int]`としています。また、`to`メソッドで取得する型を決定します。ここでは`Option`型を取得するため、`to[Option]`としています。 | Method | Return Type | Notes | |--------------|----------------|-------------------------------| From b161a60324053c25fe1f330a684b85bcdbb27031 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 28 Sep 2024 23:49:48 +0900 Subject: [PATCH 110/160] Update Selecting-Data document --- docs/src/main/mdoc/img/data_multi_select.png | Bin 0 -> 144937 bytes docs/src/main/mdoc/img/data_select.png | Bin 0 -> 75597 bytes .../main/mdoc/ja/tutorial/Selecting-Data.md | 81 +++++++++++++++++- 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 docs/src/main/mdoc/img/data_multi_select.png create mode 100644 docs/src/main/mdoc/img/data_select.png diff --git a/docs/src/main/mdoc/img/data_multi_select.png b/docs/src/main/mdoc/img/data_multi_select.png new file mode 100644 index 0000000000000000000000000000000000000000..4dadbe79d0d740a7443f31240dab8343a663b9d7 GIT binary patch literal 144937 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

)q-aV0_89@4r3r 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 Jc(Y4!{{yH0fLj0n literal 0 HcmV?d00001 diff --git a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md index a2a2359cd..80d68326e 100644 --- a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md +++ b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md @@ -41,15 +41,88 @@ sql"SELECT name, email FROM user" .foreach(println) // Unit ``` -ldbcは、複数のカラムを選択してクラスにマッピングすることもできます。 +## クラスへのマッピング -```scala -case class User(name: String, email: String) +ldbcは、複数のカラムを選択してクラスにマッピングすることもできます。これは、`User`クラスを定義して、クエリの結果を`User`クラスにマッピングする例です。 -sql"SELECT name, email FROM user" +```scala 3 +case class User(id: Long, name: String, email: String) + +sql"SELECT id, name, email FROM user" .query[User] // Query[IO, User] .to[List] // Executor[IO, List[User]] .readOnly(conn) // IO[List[User]] .unsafeRunSync() // List[User] .foreach(println) // Unit ``` + +クラスのフィールドは、クエリのカラム名と一致する必要があります。これは、`User`クラスのフィールドが`id`、`name`、`email`であるため、クエリのカラム名が`id`、`name`、`email`であることを意味します。 + +![Selecting Data](../../img/data_select.png) + +`Join`などを使用して複数のテーブルからデータを選択する方法を見てみましょう。 + +これは、`City`, `Country`, `CityWithCountry`クラスそれぞれを定義して、`city`と`country`テーブルを`Join`したクエリの結果を`CityWithCountry`クラスにマッピングする例です。 + +```scala 3 +case class City(id: Long, name: String) +case class Country(code: String, name: String, region: String) +case class CityWithCountry(coty: City, country: Country) + +sql""" + SELECT + city.id, + city.name, + country.code, + country.name, + country.region + FROM city + JOIN country ON city.country_code = country.code +""" + .query[CityWithCountry] // Query[IO, CityWithCountry] + .to[List] // Executor[IO, List[CityWithCountry]] + .readOnly(conn) // IO[List[CityWithCountry]] + .unsafeRunSync() // List[CityWithCountry] + .foreach(println) // Unit +``` + +クラスのフィールドは、クエリのカラム名と一致する必要があると先ほど述べました。 +この場合、`City`クラスのフィールドが`id`、`name`であり、`Country`クラスのフィールドが`code`、`name`、`region`であるため、クエリのカラム名が`id`、`name`、`code`、`name`、`region`であることを意味します。 + +`Join`を行った場合、それぞれのカラムはテーブル名と共に指定しどのテーブルのカラムかを明示する必要があります。 +この例では、`city.id`、`city.name`、`country.code`、`country.name`、`country.region`として指定しています。 + +ldbcではこのように`テーブル名`.`カラム名`を`クラス名`.`フィールド名`にマッピングすることによって、複数のテーブルから取得したデータをネストしたクラスにマッピングすることができます。 + +![Selecting Data](../../img/data_multi_select.png) + +ldbcでは`Join`を行い複数のテーブルからデータを取得する際に、単体のクラスのみではなくクラスの`Tuple`にマッピングすることもできます。 + +```scala 3 +case class City(id: Long, name: String) +case class Country(code: String, countryName: String, region: String) + +sql""" + SELECT + city.id, + city.name, + country.code, + country.name AS countryName, + country.region + FROM city + JOIN country ON city.country_code = country.code +""" + .query[(City, Country)] // Query[IO, (City, Country)] + .to[List] // Executor[IO, List[(City, Country)]] + .readOnly(conn) // IO[List[(City, Country)]] + .unsafeRunSync() // List[(City, Country)] + .foreach(println) // Unit +``` + +この例では、`City`クラスと`Country`クラスを`Tuple`にマッピングしています。 + +ここで注意したいのが、先ほどと異なり`テーブル名`.`カラム名`を`クラス名`.`フィールド名`にマッピングすることはできません。なぜならクラスのフィールドにはテーブル名が含まれていないためです。 + +今回の`city`と`country`テーブルのように`name`という同じ名前のカラムが存在する場合、どちらのカラムをどのクラスのフィールドにマッピングするかを判別することができません。 + +そのため、`Country`クラスの`name`カラムを`countryName`としデータを`SELECT`文で指定する際に同様に`country`テーブルの`name`カラムを`countryName`というエイリアスを指定することでマッピングできるようにしています。 From 5899cce4f62c2882a626c36c770a386ddbaf02d4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 19:29:20 +0900 Subject: [PATCH 111/160] Update document --- .../main/mdoc/ja/tutorial/Custom-Data-Type.md | 2 +- .../main/mdoc/ja/tutorial/Selecting-Data.md | 39 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md b/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md index 21e0400ca..7bd18bb23 100644 --- a/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md +++ b/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md @@ -54,7 +54,7 @@ ldbcではパラメーターの他に実行結果から独自の型を取得す 以下のコード例では、`Decoder.Elem`を使用して単一のデータ型を取得する方法を示しています。 ```scala 3 - given Decoder.Elem[Status] = Decoder.Elem.mapping[Boolean, Status] { +given Decoder.Elem[Status] = Decoder.Elem.mapping[Boolean, Status] { case true => Status.Active case false => Status.InActive } diff --git a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md index 80d68326e..db3d9eb43 100644 --- a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md +++ b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md @@ -100,29 +100,48 @@ ldbcでは`Join`を行い複数のテーブルからデータを取得する際 ```scala 3 case class City(id: Long, name: String) -case class Country(code: String, countryName: String, region: String) +case class Country(code: String, name: String, region: String) sql""" SELECT city.id, city.name, country.code, - country.name AS countryName, + country.name, country.region FROM city JOIN country ON city.country_code = country.code """ - .query[(City, Country)] // Query[IO, (City, Country)] - .to[List] // Executor[IO, List[(City, Country)]] - .readOnly(conn) // IO[List[(City, Country)]] - .unsafeRunSync() // List[(City, Country)] - .foreach(println) // Unit + .query[(City, Country)] + .to[List] + .readOnly(conn) + .unsafeRunSync() + .foreach(println) ``` この例では、`City`クラスと`Country`クラスを`Tuple`にマッピングしています。 -ここで注意したいのが、先ほどと異なり`テーブル名`.`カラム名`を`クラス名`.`フィールド名`にマッピングすることはできません。なぜならクラスのフィールドにはテーブル名が含まれていないためです。 +ここで注意したいのが、先ほどと異なり`テーブル名`.`カラム名`を`クラス名`.`フィールド名`にマッピングすること際にテーブル名はクラス名を使用しています。 -今回の`city`と`country`テーブルのように`name`という同じ名前のカラムが存在する場合、どちらのカラムをどのクラスのフィールドにマッピングするかを判別することができません。 +そのため、このマッピングには制約がありテーブル名とクラス名は等価でなければいけません。つまり、エイリアスなどを使ってテーブル名を`city`から`c`などに短縮した場合、クラス名も`C`でなければならない。 + +```scala 3 +case class C(id: Long, name: String) +case class CT(code: String, name: String, region: String) -そのため、`Country`クラスの`name`カラムを`countryName`としデータを`SELECT`文で指定する際に同様に`country`テーブルの`name`カラムを`countryName`というエイリアスを指定することでマッピングできるようにしています。 +sql""" + SELECT + c.id, + c.name, + ct.code, + ct.name, + ct.region + FROM city AS c + JOIN country AS ct ON c.country_code = ct.code +""" + .query[(City, Country)] + .to[List] + .readOnly(conn) + .unsafeRunSync() + .foreach(println) +``` From 505c614f92cf5435852f03b7ef89c9a99035cec3 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 19:37:26 +0900 Subject: [PATCH 112/160] Create index document for English --- docs/src/main/mdoc/en/index.md | 74 +++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/docs/src/main/mdoc/en/index.md b/docs/src/main/mdoc/en/index.md index 25a5207dc..946ff7a97 100644 --- a/docs/src/main/mdoc/en/index.md +++ b/docs/src/main/mdoc/en/index.md @@ -3,6 +3,76 @@ laika.metadata.language = en %} -# LDBC +# ldbc (Lepus Database Connectivity) -Note that **LDBC** is pre-1.0 software and is still under active development. Newer versions may no longer be binary compatible with earlier versions. +Please note that **ldbc** is pre-1.0 software and is still under active development. Newer versions may no longer be binary compatible with earlier versions. + +ldbc is a library for building pure functional JDBC layers by [Cats Effect 3](https://typelevel.org/cats-effect/) and [Scala 3](https://github.com/scala/scala3). + +ldbc is a [Typelevel](http://typelevel.org/) project. It embraces pure, unconventional, functional programming as described in Scala's [Code of Conduct](http://scala-lang.org/conduct.html) and is meant to provide a safe and friendly environment for teaching, learning, and contributing. + +## Introduction + +Most of our application development involves the use of databases. + +One way to access databases in Scala is to use JDBC, and there are several libraries in Scala that wrap this JDBC. + +- Functional DSLs (Slick, quill, zio-sql) +- SQL string interpolators (Anorm, doobie) + +ldbc is another library that also wraps JDBC, and ldbc combines aspects of each to provide a type-safe and refactorable SQL interface in the Scala 3 library, allowing SQL expressions to be expressed on MySQL databases. + +Unlike other libraries, ldbc also provides its own connector built in Scala. + +Scala currently supports multiple platforms: JVM, JS, and Native. + +However, if the library uses JDBC, it will only work in a JVM environment. + +Therefore, ldbc is being developed to provide a connector written in Scala that is compatible with the MySQL protocol so that it can work on different platforms. +With ldbc, database access can be done regardless of platform while taking advantage of Scala's type safety and functional programming. + +Also, the use of ldbc allows development to centralize Scala models, sql schemas, and documentation by managing a single resource. + +This concept was inspired by [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. tapir can be used to build type-safe endpoints, which can then be used to generate OpenAPI documents, OpenAPI documents can also be generated from the constructed endpoints. + +ldbc uses Scala at the database layer to allow for the same type-safe construction and document generation using the construction. + +### Target Audience + +This document is intended for developers who use ldbc, a library for database access using the Scala programming language. + +ldbc is designed for those interested in typed, pure functional programming. If you are not a Cats user or are not familiar with functional I/O or the monadic Cats Effect, you may need to proceed slowly. + +Nevertheless, if you are confused or frustrated by this documentation or the ldbc API, please submit an issue and ask for help. Because both the library and the documentation are new and rapidly changing, it is inevitable that there will be some unclear points. Therefore, this document will be continually updated to address problems and omissions. + +## Quick Start + +The current version is **@VERSION@** corresponding to **Scala @SCALA_VERSION@**. + +```scala +libraryDependencies ++= Seq( + + // Start with this one. + "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", + + // Select the connector to be used + "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@", // Java Connector (Supported platforms: JVM) + "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@", // Scala connector (supported platforms: JVM, JS, Native) + + // Then add these as needed + "@ORGANIZATION@" %% "ldbc-query-builder" % "@VERSION@", // Type-safe query construction + "@ORGANIZATION@" %% "ldbc-schema" % "@VERSION@", // Building a database schema +) +``` + +## TODO + +- JSON data type support +- SET data type support +- Support for Geometry data type +- Support for CHECK constraints +- Non-MySQL database support +- Streaming Support +- ZIO module support +- Test Kit +- etc... From 7658019d3e40883b9eec565ead298c8b4660085c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 19:43:36 +0900 Subject: [PATCH 113/160] Create Setup document for English --- docs/src/main/mdoc/en/tutorial/Setup.md | 183 ++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Setup.md diff --git a/docs/src/main/mdoc/en/tutorial/Setup.md b/docs/src/main/mdoc/en/tutorial/Setup.md new file mode 100644 index 000000000..e714a0998 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Setup.md @@ -0,0 +1,183 @@ +{% + laika.title = Setup + laika.metadata.language = en +%} + +# Setup + +Welcome to the wonderful world of ldbc! In this section we will help you get everything set up. + +## Database Setup + +First, start the database using Docker. Use the following code to start the database + +```yaml +services: + mysql: + image: mysql:@MYSQL_VERSION@ + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` + +Next, initialize the database. + +Create the database as shown in the code below. + +```sql +CREATE DATABASE IF NOT EXISTS sandbox_db; +``` + +Next, tables are created. + +```sql +CREATE TABLE IF NOT EXISTS `user` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(50) NOT NULL, + `email` VARCHAR(100) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `product` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(100) NOT NULL, + `price` DECIMAL(10, 2) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) + +CREATE TABLE IF NOT EXISTS `order` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `product_id` INT NOT NULL, + `order_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `quantity` INT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES `user` (id), + FOREIGN KEY (product_id) REFERENCES `product` (id) +) +``` + +Insert data into each table. + +```sql +INSERT INTO user (name, email) VALUES + ('Alice', 'alice@example.com'), + ('Bob', 'bob@example.com'), + ('Charlie', 'charlie@example.com'); + +INSERT INTO product (name, price) VALUES + ('Laptop', 999.99), + ('Mouse', 19.99), + ('Keyboard', 49.99), + ('Monitor', 199.99); + +INSERT INTO `order` (user_id, product_id, quantity) VALUES + (1, 1, 1), -- Alice ordered 1 Laptop + (1, 2, 2), -- Alice ordered 2 Mice + (2, 3, 1), -- Bob ordered 1 Keyboard + (3, 4, 1); -- Charlie ordered 1 Monitor +``` + +## Scala Setup + +The tutorial will use [Scala CLI](https://scala-cli.virtuslab.org/). Therefore, you will need to install the Scala CLI. + +```bash +brew install Virtuslab/scala-cli/scala-cli +``` + +**Execute with Scala CLI** + +The database setup described earlier can be performed using the Scala CLI. The following commands can be used to perform this setup. + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` + +### First Program + +To begin, create a new project with ldbc as a dependency. + +```scala +//> using scala "@SCALA_VERSION@" +//> using dep "@ORGANIZATION@::ldbc-dsl:@VERSION@" +``` + +Before using ldbc, some symbols need to be imported. For convenience, we will use package import here. This will give us the symbols most commonly used when working with high-level APIs. + +```scala +import ldbc.dsl.io.* +``` + +Let's bring Cats too. + +```scala +import cats.syntax.all.* +import cats.effect.* +``` + +Next, tracers and log handlers are provided. These are used to log applications. Tracers are used to record application traces. The log handler is used to log the application. + +The following code provides tracers and log handlers but does nothing with the entities. + +```scala 3 +given Tracer[IO] = Tracer.noop[IO] +given LogHandler[IO] = LogHandler.noop[IO] +``` + +The most common type handled by the ldbc high-level API is of the form `Executor[F, A]`, which specifies a calculation to be performed in a context where `{java | ldbc}.sql.Connection` is available, ultimately producing a value of type A. + +Let's start with an Executor program that only returns constants. + +```scala +val program: Executor[IO, Int] = Executor.pure[IO, Int](1) +``` + +Next, create a connector to connect to the database. A connector is a resource for managing connections to a database. A connector provides resources to initiate a connection to the database, execute a query, and close the connection. + +Here, ldbc uses a connector created by ldbc on its own. How to select and create a connector will be explained later. + +```scala +def connection = Connection[IO]( + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password"), + ssl = SSL.Trusted +) +``` + +Executor is a data type that knows how to connect to a database, how to pass connections, and how to clean up connections, and this knowledge allows Executor to be converted to IO to obtain an executable program. Specifically, execution yields an IO that connects to the database and executes a single transaction. + +```scala +connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() +``` + +Hooray! I was able to calculate the constants. This is not very interesting, since we will not be asking the database to do the work, but the first step is complete. + +> Remember that all the code in this book is pure except for the call to IO.unsafeRunSync. IO.unsafeRunSync is an “end of the world” operation that usually appears only at the entry point of an application. The REPL forces the calculation to to use this to make it “happen.” + +**Execute with Scala CLI** + +This program can also be run using the Scala CLI. The following command will execute this program. + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` From 28b915a5930b98d2345c14740377fa127b91677e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 19:48:36 +0900 Subject: [PATCH 114/160] Create Connection document for English --- docs/src/main/mdoc/en/tutorial/Connection.md | 93 ++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Connection.md diff --git a/docs/src/main/mdoc/en/tutorial/Connection.md b/docs/src/main/mdoc/en/tutorial/Connection.md new file mode 100644 index 000000000..cf1aea9fa --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Connection.md @@ -0,0 +1,93 @@ +{% + laika.title = Connection + laika.metadata.language = en +%} + +# Connection + +This chapter describes how to build a connection to connect to a database. + +To connect to a database, a connection must be established. A connection is a resource that manages the connection to the database. A connection provides the resources to initiate a connection to the database, execute a query, and close the connection. + +ldbc connects to the database using either jdbc or ldbc's own connector. Which one to use depends on the dependencies you set up. + +## Use jdbc connector + +First, add the dependencies. + +If you use the JDJD connector, you must also add the MySQL connector. + +```scala +//> dep "@ORGANIZATION@::jdbc-connector:@VERSION@" +//> dep "com.mysql":"mysql-connector-j":"@MYSQL_VERSION@" +``` + +Next, create a data source using `MysqlDataSource`. + +```scala +val ds = new com.mysql.cj.jdbc.MysqlDataSource() +ds.setServerName("127.0.0.1") +ds.setPortNumber(13306) +ds.setDatabaseName("world") +ds.setUser("ldbc") +ds.setPassword("password") +``` + +Create a jdbc connector data source using the data source you created. + +```scala +val datasource = jdbc.connector.MysqlDataSource[IO](ds) +``` + +Finally, a connection is created using a jdbc connector. + +```scala +val connection: Resource[IO, Connection[IO]] = + Resource.make(datasource.getConnection)(_.close()) +``` + +Here we use the Cats Effect `Resource` to close the connection after it has been used. + +## Use ldbc connector + +First, add dependencies. + +```scala +//> dep "@ORGANIZATION@::ldbc-connector:@VERSION@" +``` + +Next, Tracer is provided. ldbc connectors use Tracer to collect telemetry data. These are used to record application traces. + +Here, Tracer is provided using `Tracer.noop`. + +```scala 3 +given Tracer[IO] = Tracer.noop[IO] +``` + +Finally, create a `Connection`. + +```scala +val connection: Resource[IO, Connection[IO]] = + ldbc.connector.Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "ldbc", + password = Some("password"), + database = Some("ldbc") + ) +``` + +The parameters for setting up a connection are as follows + +| Property | Detail | Required | +|---------------------------|-------------------------------------------------------------------------------|----------| +| `host` | `Database Host Information` | ✅ | +| `port` | `Database Port Information` | ✅ | +| `user` | `Database User Information` | ✅ | +| `password` | `Database password information (default: None)` | ❌ | +| `database` | `Database name information (default: None)` | ❌ | +| `debug` | `Whether to display debugging information or not (default: false)` | ✅ | +| `ssl` | `SSL configuration (default: SSL.None)` | ✅ | +| `socketOptions` | `Specify socket options for TCP/ UDP sockets (default: defaultSocketOptions)` | ✅ | +| `readTimeout` | `Specify timeout period (default: Duration.Inf)` | ✅ | +| `allowPublicKeyRetrieval` | `Whether to retrieve the public key or not (default: false)` | ✅ | From b3038ac5709d650902435830183f1d87ec8398e0 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 19:52:58 +0900 Subject: [PATCH 115/160] Create Simple Program document for English --- .../main/mdoc/en/tutorial/Simple-Program.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Simple-Program.md diff --git a/docs/src/main/mdoc/en/tutorial/Simple-Program.md b/docs/src/main/mdoc/en/tutorial/Simple-Program.md new file mode 100644 index 000000000..027fd52ed --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Simple-Program.md @@ -0,0 +1,106 @@ +{% + laika.title = Simple Program + laika.metadata.language = en +%} + +# Simple Program + +In this section, we first explain the basic usage of ldbc by creating and executing a simple program. + +※ The program environment used here is assumed to be the one built in the setup. + +## The first program + +In this program, we will create a program that connects to a database and retrieves the results of a calculation. + +Now, let's create a query that asks the database to calculate a constant using `sql string interpolator`. + +```scala +val program: Executor[IO, Option[Int]] = sql"SELECT 2".query[Int].to[Option] +``` + +Queries created with `sql string interpolator` use the `query` method to determine the type to retrieve. Here, `query[Int]` is used to get the `Int` type. Also, the `to` method determines the type to retrieve. Here, `to[Option]` is used to get the `Option` type. + +| Method | Return Type | Notes | +|--------------|----------------|-----------------------------------------------------| +| `to[List]` | `F[List[A]]` | `View all results in a list` | +| `to[Option]` | `F[Option[A]]` | `Result is 0 or 1, otherwise an error is generated` | +| `unsafe` | `F[A]` | `Exactly one result, otherwise an error will occur` | + +Finally, write a program that connects to the database and returns a value. This program connects to the database, executes the query, and retrieves the results. + +```scala +connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() +``` + +Connected to database to calculate constants. Quite impressive. + +**Execute with Scala CLI** + +This program can also be run using the Scala CLI. The following command will execute this program. + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` + +## Second program + +What if you want to do multiple things in one transaction? Easy! Since Executor is a monad, you can use for comprehensions to make two small programs into one big program. + +```scala 3 +val program: Executor[IO, (List[Int], Option[Int], Int)] = + for + result1 <- sql"SELECT 1".query[Int].to[List] + result2 <- sql"SELECT 2".query[Int].to[Option] + result3 <- sql"SELECT 3".query[Int].unsafe + yield (result1, result2, result3) +``` + +Finally, write a program that connects to the database and returns a value. This program connects to the database, executes the query, and retrieves the results. + +```scala +connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() +``` + +**Run with Scala CLI**. + +This program can also be run using the Scala CLI. The following command will execute this program. + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` + +## Third program + +Let's write a program that writes to a database. Here, we connect to the database, execute a query, and insert data. + +```scala +val program: Executor[IO, Int] = + sql"INSERT INTO user (name, email) VALUES ('Carol', 'carol@example.com')".update +``` + +The difference from the previous step is that the `commit` method is called. This commits the transaction and inserts the data into the database. + +```scala +connection + .use { conn => + program.commit(conn).map(println(_)) + } + .unsafeRunSync() +``` + +**Execute with Scala CLI**. + +This program can also be run using the Scala CLI. The following command will execute this program. + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` From ede8732f04333700d3dd875dbc6ae0e7b96dd155 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 19:55:22 +0900 Subject: [PATCH 116/160] Create Database Operation document for English --- .../mdoc/en/tutorial/Database-Operations.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Database-Operations.md diff --git a/docs/src/main/mdoc/en/tutorial/Database-Operations.md b/docs/src/main/mdoc/en/tutorial/Database-Operations.md new file mode 100644 index 000000000..0bf96a3dd --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Database-Operations.md @@ -0,0 +1,53 @@ +{% + laika.title = Database Operation + laika.metadata.language = en +%} + +# Database Operation + +This section describes database operations. + +Before making a database connection, you need to configure settings such as commit timing and read/write-only. + +## Read Only + +Use the `readOnly` method to initiate a read-only transaction. + +The `readOnly` method can be used to make the processing of a query to be executed read-only. The `readOnly` method can also be used with `insert/update/delete` statements, but it will result in an error at runtime because of the write operation. + +```scala +val read = sql"SELECT 1".query[Int].to[Option].readOnly(connection) +``` + +## Writing + +To write, use the `commit` method. + +The `commit` method can be used to set up the processing of a query to be committed at each query execution. + +```scala +val write = sql"INSERT INTO `table`(`c1`, `c2`) VALUES ('column 1', 'column 2')".update.commit(connection) +``` + +## Transaction + +Use the `transaction` method to initiate a transaction. + +The `transaction` method can be used to combine multiple database connection operations into a single transaction. + +ldbc will build a process to connect to the database in the form of `Executor[F, A]`. Since Executor is a monad, two small programs can be combined into one large program using for comprehensions. + +```scala 3 +val program: Executor[IO, (List[Int], Option[Int], Int)] = + for + result1 <- sql"SELECT 1".query[Int].to[List] + result2 <- sql"SELECT 2".query[Int].to[Option] + result3 <- sql"SELECT 3".query[Int].unsafe + yield (result1, result2, result3) +``` + +The `transaction` method can be used to combine `Executor` programs into a single transaction. + +```scala +val transaction = program.transaction(connection) +``` From f618fcd4d30043e086b02f16a0e790a53f901d4b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 19:59:35 +0900 Subject: [PATCH 117/160] Create Parameterized Queries document for English --- .../mdoc/en/tutorial/Parameterized-Queries.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md diff --git a/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md b/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md new file mode 100644 index 000000000..e718d6890 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md @@ -0,0 +1,112 @@ +{% + laika.title = Parameter + laika.metadata.language = ja +%} + +# Parameterized Queries + +In this chapter, you will learn how to construct parameterized queries. + +## Adding parameters + +First, create a query with no parameters. + +```scala +sql"SELECT name, email FROM user".query[(String, String)].to[List] +``` + +Next, let's incorporate the query into a method and add a parameter to select only the data that matches the `id` specified by the user. Insert the `id` argument as `$id` into the SQL statement, just as you would interpolate a string. + +```scala +val id = 1 + +sql"SELECT name, email FROM user WHERE id = $id".query[(String, String)].to[List] +``` + +Querying with connections works fine. + +```scala +connection.use { conn => + sql"SELECT name, email FROM user WHERE id = $id" + .query[(String, String)] + .to[List] + .readOnly(conn) +} +``` + +What is happening here? It looks like we are just dropping a string literal into an SQL string, but we are actually building a `PreparedStatement` and the `id` value is eventually set by a call to `setInt`. + +## Multiple parameters + +Multiple parameters work the same way. No surprises. + +```scala +val id = 1 +val email = "alice@example.com" + +connection.use { conn => + sql"SELECT name, email FROM user WHERE id = $id AND email > $email" + .query[(String, String)] + .to[List] + .readOnly(conn) +} +``` + +## Handling IN Clauses + +A common irritation when dealing with SQL literals is the desire to inline a series of arguments into an IN clause, but SQL does not support this concept (nor does JDBC support anything). + +```scala +val ids = NonEmptyList.of(1, 2, 3) + +connection.use { conn => + (sql"SELECT name, email FROM user WHERE" ++ in("id", ids)) + .query[(String, String)] + .to[List] + .readOnly(conn) +} +``` + +Note that the `ids` is `NonEmptyList` since the IN clause must not be empty. + +Executing this query yields the desired result + +ldbc provides several other useful functions. + +- `values` - Creates a VALUES clause. +- `in` - Creates an IN clause. +- `notIn` - Creates a NOT IN clause. +- `and` - Generates an AND clause. +- `or` - Generates an OR clause. +- `whereAnd` - Generates a WHERE clause with multiple conditions enclosed in AND clauses. +- `whereOr` - Generates WHERE clauses for multiple conditions enclosed in OR clauses. +- `set` - Generates a SET clause. +- `orderBy` - Generates an ORDER BY clause. + +## Static parameters + +Although parameters are dynamic, sometimes you may want to use them as parameters but treat them as static values. + +For example, to change the column to be retrieved based on the value received, you can write the following + +```scala +val column = "name" + +sql"SELECT $column FROM user".query[String].to[List] +``` + +Dynamic parameters are handled by `PreparedStatement`, so the query string itself is replaced by `? `. + +Thus, the query will be executed as `SELECT ? FROM user`. + +This makes it difficult to understand the query output in the log, so if you want to treat `$column` as a static value, set `$column` to `${sc(column)}` so that it is directly embedded in the query string. + +```scala +val column = "name" + +sql"SELECT ${sc(column)} FROM user".query[String].to[List] +``` + +This query is executed as `SELECT name FROM user`. + +> `sc(...)` Note that does not escape the passed string. Passing user-supplied data is an injection risk. From 9862abad93e87f29d8a23c08998922b1cf63c17a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:04:08 +0900 Subject: [PATCH 118/160] Create Selecting Data document for English --- .../mdoc/en/tutorial/Parameterized-Queries.md | 2 +- .../main/mdoc/en/tutorial/Selecting-Data.md | 147 ++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/mdoc/en/tutorial/Selecting-Data.md diff --git a/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md b/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md index e718d6890..11c3f2292 100644 --- a/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md +++ b/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md @@ -1,6 +1,6 @@ {% laika.title = Parameter - laika.metadata.language = ja + laika.metadata.language = en %} # Parameterized Queries diff --git a/docs/src/main/mdoc/en/tutorial/Selecting-Data.md b/docs/src/main/mdoc/en/tutorial/Selecting-Data.md new file mode 100644 index 000000000..d67dc9887 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Selecting-Data.md @@ -0,0 +1,147 @@ +{% + laika.title = Data Select + laika.metadata.language = en +%} + +# Data Select + +This chapter describes how to select data using the ldbc data set. + +## Loading rows into a collection + +For the first query, let's aim for a low-level query, select a few users to list, and print out the first few cases. There are several steps here, so I will note the types along the way. + +```scala +sql"SELECT name FROM user" + .query[String] // Query[IO, String] + .to[List] // Executor[IO, List[String]] + .readOnly(conn) // IO[List[String]] + .unsafeRunSync() // List[String] + .foreach(println) // Unit +``` + +Let's break this down a bit. + +- `sql “SELECT name FROM user”.query[String]` is a single-column query that defines `Query[IO, String]` and maps each row returned to a String. This query is a single-column query that maps each row returned to a String. +- `.to[List]` is a convenience method that accumulates rows into a list, in this case `Executor[IO, List[String]]`. This method works for all collection types with CanBuildFrom. +- `readOnly(conn)` generates `IO[List[String]]`, which when executed will output a normal Scala `List[String]`. +- `unsafeRunSync()` executes the IO monad and gets the result. This is used to execute the IO monad and get the result. +- `foreach(println)` prints out each element of the list. + +## Multiple column query + +Of course, multiple columns can be selected and mapped to tuples. + +```scala +sql"SELECT name, email FROM user" + .query[(String, String)] // Query[IO, (String, String)] + .to[List] // Executor[IO, List[(String, String)]] + .readOnly(conn) // IO[List[(String, String)]] + .unsafeRunSync() // List[(String, String)] + .foreach(println) // Unit +``` + +## Mapping to classes + +ldbc also allows you to select multiple columns and map them to classes. This is an example of defining a `User` class and mapping the query results to the `User` class. + +```scala 3 +case class User(id: Long, name: String, email: String) + +sql"SELECT id, name, email FROM user" + .query[User] // Query[IO, User] + .to[List] // Executor[IO, List[User]] + .readOnly(conn) // IO[List[User]] + .unsafeRunSync() // List[User] + .foreach(println) // Unit +``` + +The fields of the class must match the column names of the query. This means that the fields of the `User` class are `id`, `name`, and `email`, so the query column names must be `id`, `name`, and `email`. + +![Selecting Data](../../img/data_select.png) + +Let's see how to select data from multiple tables using `Join` and other methods. + +This is an example of defining `City`, `Country`, and `CityWithCountry` classes, respectively, and mapping the results of a query that `Joins` the `city` and `country` tables to the `CityWithCountry` class. + +```scala 3 +case class City(id: Long, name: String) +case class Country(code: String, name: String, region: String) +case class CityWithCountry(coty: City, country: Country) + +sql""" + SELECT + city.id, + city.name, + country.code, + country.name, + country.region + FROM city + JOIN country ON city.country_code = country.code +""" + .query[CityWithCountry] // Query[IO, CityWithCountry] + .to[List] // Executor[IO, List[CityWithCountry]] + .readOnly(conn) // IO[List[CityWithCountry]] + .unsafeRunSync() // List[CityWithCountry] + .foreach(println) // Unit +``` + +We mentioned earlier that the fields of the class must match the column names of the query. +In this case, the fields of the `City` class are `id` and `name`, and the fields of the `Country` class are `code`, `name`, and `region`, so the query column names are `id`, `name`, `code`, `name`, and `region means that the query column names are + +If you do a `Join`, each column must be specified with a table name to indicate which table the column is from. +In this example, the columns are specified as `city.id`, `city.name`, `country.code`, `country.name`, and `country.region`. + +In ldbc, this is how `table name`. `column name` to `class name`. By mapping `field names` to `field names`, data from multiple tables can be mapped to nested classes. + +![Selecting Data](../../img/data_multi_select.png) + +In ldbc, when performing a `Join` to retrieve data from multiple tables, it is possible to map to a class `Tuple` instead of only a single class. + +```scala 3 +case class City(id: Long, name: String) +case class Country(code: String, name: String, region: String) + +sql""" + SELECT + city.id, + city.name, + country.code, + country.name, + country.region + FROM city + JOIN country ON city.country_code = country.code +""" + .query[(City, Country)] + .to[List] + .readOnly(conn) + .unsafeRunSync() + .foreach(println) +``` + +In this example, the `City` and `Country` classes are mapped to `Tuple`. + +It is important to note that, unlike the previous example, the `Table Name`. `Column Name` to `Class Name`. Field Name`, the table name is the class name. + +Therefore, there is a restriction on this mapping: table names and class names must be equivalent. In other words, if you shorten the table name from `city` to `c`, etc., using aliases, etc., the class name must also be `C`. + +```scala 3 +case class C(id: Long, name: String) +case class CT(code: String, name: String, region: String) + +sql""" + SELECT + c.id, + c.name, + ct.code, + ct.name, + ct.region + FROM city AS c + JOIN country AS ct ON c.country_code = ct.code +""" + .query[(City, Country)] + .to[List] + .readOnly(conn) + .unsafeRunSync() + .foreach(println) +``` From 2f13e84b0419700f00da20c1dd15830f01b24ae5 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:07:43 +0900 Subject: [PATCH 119/160] Create Updating Data document for English --- .../main/mdoc/en/tutorial/Updating-Data.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Updating-Data.md diff --git a/docs/src/main/mdoc/en/tutorial/Updating-Data.md b/docs/src/main/mdoc/en/tutorial/Updating-Data.md new file mode 100644 index 000000000..4050b6b3b --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Updating-Data.md @@ -0,0 +1,115 @@ +{% + laika.title = Data Update + laika.metadata.language = en +%} + +# Data Update + +This chapter describes operations to modify data in the database and how to retrieve the updated results. + +## Insert + +Insert is simple and works just like select. Here we define a method to create an `Executor` that will insert rows into the `user` table. + +```scala +def insertUser(name: String, email: String): Executor[IO, Int] = + sql"INSERT INTO user (name, email) VALUES ($name, $email)" + .update +``` + +Let's insert a line. + +```scala +insertUser("dave", "dave@example.com").commit.unsafeRunSync() +``` + +And then read it back. + +```scala +sql"SELECT * FROM user" + .query[(Int, String, String)] // Query[IO, (Int, String, String)] + .to[List] // Executor[IO, List[(Int, String, String)]] + .readOnly(conn) // IO[List[(Int, String, String)]] + .unsafeRunSync() // List[(Int, String, String)] + .foreach(println) // Unit +``` + +## Update + +The same pattern applies to updates. Here we update the user's email address. + +```scala +def updateUserEmail(id: Int, email: String): Executor[IO, Int] = + sql"UPDATE user SET email = $email WHERE id = $id" + .update +``` + +Getting Results + +```scala +updateUserEmail(1, "alice+1@example.com").commit.unsafeRunSync() + +sql"SELECT * FROM user WHERE id = 1" + .query[(Int, String, String)] // Query[IO, (Int, String, String)] + .to[Option] // Executor[IO, List[(Int, String, String)]] + .readOnly(conn) // IO[List[(Int, String, String)]] + .unsafeRunSync() // List[(Int, String, String)] + .foreach(println) // Unit +// Some((1,alice,alice+1@example.com)) +``` + +## Auto-generated keys + +When inserting, we want to return the newly generated key. We do this the hard way, first by inserting and getting the last generated key with `LAST_INSERT_ID` and then selecting the specified row. + +```scala 3 +def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = + for + _ <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".update + id <- sql"SELECT LAST_INSERT_ID()".query[Int].unsafe + task <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] + yield task +``` + +```scala +insertUser("eve", "eve@example.com").commit.unsafeRunSync() +``` + +This is frustrating, but supported by all databases (although the “get last used ID” feature varies from vendor to vendor). + +In MySQL, only rows with `AUTO_INCREMENT` set can be returned on insert. The above operation can be reduced to two statements + +If you are inserting rows using an auto-generated key, you can use the `returning` method to retrieve the auto-generated key. + +```scala 3 +def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = + for + id <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".returning[Int] + user <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] + yield user +``` + +```scala +insertUser("frank", "frank@example.com").commit.unsafeRunSync() +``` + +## Batch update + +To perform batch updates, define an `insertManyUser` method that inserts multiple rows using `NonEmptyList`. + +```scala +def insertManyUser(users: NonEmptyList[(String, String)]): Executor[IO, Int] = + val value = users.map { case (name, email) => sql"($name, $email)" } + (sql"INSERT INTO user (name, email) VALUES" ++ values(value)).update +``` + +Running this program gives the updated row count. + +```scala +val users = NonEmptyList.of( + ("greg", "greg@example.com"), + ("henry", "henry@example.com") +) + +insertManyUser(users).commit.unsafeRunSync() +``` From 42eb67c9bfe460a427a35aa866d068f91d25b86b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:12:06 +0900 Subject: [PATCH 120/160] Create Error Handling document for English --- .../main/mdoc/en/tutorial/Error-Handling.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Error-Handling.md diff --git a/docs/src/main/mdoc/en/tutorial/Error-Handling.md b/docs/src/main/mdoc/en/tutorial/Error-Handling.md new file mode 100644 index 000000000..47836b400 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Error-Handling.md @@ -0,0 +1,37 @@ +{% + laika.title = Error Handling + laika.metadata.language = en +%} + +# Error Handling + +This chapter examines a set of combinators for building programs that trap and handle exceptions. + +## About Exceptions + +Whether an operation succeeds or not depends on unpredictable factors such as the health of the network, the current contents of the table, and the state of the lock. Therefore, we must decide whether to compute everything in a logical OR like `EitherT[Executor, Throwable, A]` or to allow exception propagation until it is explicitly caught. In other words, when an ldbc action (which is converted to a target monad) is executed, an exception may be raised. + +There are three main types of exceptions that are likely to occur + +1. various types of IOExceptions can occur with all types of I/O, and these exceptions tend to be unrecoverable +2. database exceptions usually occur in common situations such as key violations, as a general SQLException that identifies a specific error in vendor-specific SQLState. Error codes must be communicated as lore or discovered by experimentation; there are XOPEN and SQL:2003 standards, but no vendor seems to adhere to these specifications. Some of these errors are recoverable, some are not. +3. ldbc raises InvariantViolation for invalid type mappings, unknown JDBC constants returned by the driver, observed NULL values, and other violations of immutable conditions assumed by ldbc. These exceptions indicate programmer error or driver incompatibility and are generally unrecoverable. + +## Monad errors and derived combinators + +All ldbc monads are derived from the `MonadError[?[_], Throwable]` and provide an Async instance that extends it. This means that Executor and others will have the following primitive operations + +- raiseError: raise an exception (convert Throwable' to `M[A]`) +- handleErrorWith: handle an exception (convert `M[A]` to `M[B]`) +- attempt: catch exception (convert `M[A]` to `M[Either[Throwable, A]]`) + +In other words, any ldbc program can catch an exception simply by adding `attempt`. + +```scala +val program = Executor.pure[IO, Int](1) + +program.attempt +// Executor[IO, Either[Throwable, Int]] +``` + +From the `attempt` and `raiseError` combinators, many other operations can be derived, as described in the Cats documentation. From 806b96f39375a567f2b5c3f6a7070d0a69711484 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:12:59 +0900 Subject: [PATCH 121/160] Create Logging document for English --- docs/src/main/mdoc/en/tutorial/Logging.md | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Logging.md diff --git a/docs/src/main/mdoc/en/tutorial/Logging.md b/docs/src/main/mdoc/en/tutorial/Logging.md new file mode 100644 index 000000000..c00a94b33 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Logging.md @@ -0,0 +1,52 @@ +{% + laika.title = ロギング + laika.metadata.language = ja +%} + +# ロギング + +ldbcではデータベース接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 + +標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 + +```scala 3 +given LogHandler[IO] = LogHandler.console[IO] +``` + +任意のロギングライブラリを使用してログをカスタマイズする場合は`ldbc.dsl.logging.LogHandler`を使用します。 + +以下は標準実装のログ実装です。ldbcではデータベース接続で以下3種類のイベントが発生します。 + +- Success: 処理の成功 +- ProcessingFailure: データ取得後もしくはデータベース接続前の処理のエラー +- ExecFailure: データベースへの接続処理のエラー + +それぞれのイベントでどのようなログを書き込むかをパターンマッチングによって振り分けを行います。 + +```scala 3 +def console[F[_]: Console: Sync]: LogHandler[F] = + case LogEvent.Success(sql, args) => + Console[F].println( + s"""Successful Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) + case LogEvent.ProcessingFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed ResultSet Processing: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) + case LogEvent.ExecFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) +``` From a33198d1456977c87e1b94c7582e735499110261 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:13:41 +0900 Subject: [PATCH 122/160] Create Logging document for English --- docs/src/main/mdoc/en/tutorial/Logging.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/main/mdoc/en/tutorial/Logging.md b/docs/src/main/mdoc/en/tutorial/Logging.md index c00a94b33..ff9cb1f2b 100644 --- a/docs/src/main/mdoc/en/tutorial/Logging.md +++ b/docs/src/main/mdoc/en/tutorial/Logging.md @@ -1,27 +1,27 @@ {% - laika.title = ロギング - laika.metadata.language = ja + laika.title = Logging + laika.metadata.language = en %} -# ロギング +# Logging -ldbcではデータベース接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 +ldbc can export execution and error logs of database connections in any format using any logging library. -標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 +The standard logger using Cats Effect's Console is provided and can be used during development. ```scala 3 given LogHandler[IO] = LogHandler.console[IO] ``` -任意のロギングライブラリを使用してログをカスタマイズする場合は`ldbc.dsl.logging.LogHandler`を使用します。 +Use `ldbc.dsl.logging.LogHandler` to customize logging using any logging library. -以下は標準実装のログ実装です。ldbcではデータベース接続で以下3種類のイベントが発生します。 +The following is the standard implementation of logging. ldbc generates the following three types of events on database connection -- Success: 処理の成功 -- ProcessingFailure: データ取得後もしくはデータベース接続前の処理のエラー -- ExecFailure: データベースへの接続処理のエラー +- Success: Success of processing +- ProcessingFailure: Error in processing after getting data or before connecting to the database +- ExecFailure: Error in the process of connecting to the database -それぞれのイベントでどのようなログを書き込むかをパターンマッチングによって振り分けを行います。 +Each event is sorted by pattern matching to determine what kind of log to write. ```scala 3 def console[F[_]: Console: Sync]: LogHandler[F] = From f9dcdb8be619eae62b618ca21e6d983e9e6d4253 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:15:40 +0900 Subject: [PATCH 123/160] Create Custom Data Type document for English --- .../main/mdoc/en/tutorial/Custom-Data-Type.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md diff --git a/docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md b/docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md new file mode 100644 index 000000000..89b557ce3 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md @@ -0,0 +1,67 @@ +{% + laika.title = Custom data type + laika.metadata.language = en +%} + +# Custom data type + +This chapter describes how to use user-specific or unsupported types in table definitions built with ldbc. + +Add a new column to the table definition created in setup. + +```sql +ALTER TABLE user ADD COLUMN status BOOLEAN NOT NULL DEFAULT TRUE; +``` + +## Encoder + +In ldbc, the value to be passed to a statement is represented by an `Encoder`. The `Encoder` is a trait to represent the value to be bound to a statement. + +By implementing `Encoder`, values passed to statements can be expressed as custom types. + +Add `Status` to the user information to represent the user's status. + +```scala 3 +enum Status(val done: Boolean, val name: String): + case Active extends Status(false, "Active") + case InActive extends Status(true, "InActive") +``` +The following code example defines a custom type `Encoder`. + +This allows binding custom types to statements. + +```scala 3 +given Encoder[Status] with + override def encode(status: Status): Boolean = status.done +``` + +Custom types can be bound to statements just like any other parameter. + +```scala +val program1: Executor[IO, Int] = + sql"INSERT INTO user (name, email, status) VALUES (${ "user 1" }, ${ "user@example.com" }, ${ Status.Active })".update +``` + +Now you can bind custom types to statements. + +## Decoder + +In addition to parameters, ldbc provides a `Decoder` to get a unique type from the execution result. + +By implementing the `Decoder`, you can get your own type from the result of statement execution. + +The following code example shows how to use `Decoder.Elem` to get a single data type. + +```scala 3 +given Decoder.Elem[Status] = Decoder.Elem.mapping[Boolean, Status] { + case true => Status.Active + case false => Status.InActive +} +``` + +```scala 3 +val program2: Executor[IO, (String, String, Status)] = + sql"SELECT name, email, status FROM user WHERE id = 1".query[(String, String, Status)].unsafe +``` + +Now you can get a custom type from the execution result of a statement. From 05d52cf95721cce576a2d1d7b99a1d8f012e10fd Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:21:16 +0900 Subject: [PATCH 124/160] Create Query Builder document for English --- .../main/mdoc/en/tutorial/Query-Builder.md | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Query-Builder.md diff --git a/docs/src/main/mdoc/en/tutorial/Query-Builder.md b/docs/src/main/mdoc/en/tutorial/Query-Builder.md new file mode 100644 index 000000000..29a97d7c7 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Query-Builder.md @@ -0,0 +1,392 @@ +{% + laika.title = Query Builder + laika.metadata.language = en +%} + +# Query Builder + +This chapter describes how to build a type-safe query. + +The following dependencies must be set up in your project + +```scala +//> using dep "@ORGANIZATION@::ldbc-query-builder:@VERSION@" +``` + +In ldbc, classes are used to construct queries. + +```scala 3 +import ldbc.query.builder.* + +case class User(id: Int, name: String, email: String) derives Table +``` + +The `User` class inherits from the `Table` trace. Because the `Table` trace inherits from the `Table` class, methods of the `Table` class can be used to construct queries. + +```scala +val query = Table[User] + .select(user => (user.id, user.name, user.email)) + .where(_.email === "alice@example.com") +``` + +## SELECT + +A type-safe way to construct a SELECT statement is to use the `select` method provided by Table. ldbc is implemented to resemble a plain query, making query construction intuitive. It is also easy to see at a glance what kind of query is being constructed. + +To construct a SELECT statement that retrieves only specific columns, simply specify the columns you want to retrieve in the `select` method. + +```scala +val select = Table[User].select(_.id) + +select.statement === "SELECT id FROM user" +``` + +To specify multiple columns, simply specify the columns you wish to retrieve using the `select` method and return a tuple of the specified columns. + +```scala +val select = Table[User].select(user => (user.id, user.name)) + +select.statement === "SELECT id, name FROM user" +``` + +全てのカラムを指定したい場合はTableが提供する`selectAll`メソッドを使用することで構築できます。 + +```scala +val select = Table[User].selectAll + +select.statement === "SELECT id, name, email FROM user" +``` + +If you want to get the number of a specific column, you can construct it by using `count` on the specified column.  + +```scala +val select = Table[User].select(_.id.count) + +select.statement === "SELECT COUNT(id) FROM user" +``` + +### WHERE + +A type-safe way to set a Where condition in a query is to use the `where` method. + +```scala +val where = Table[User].selectAll.where(_.email === "alice@example.com") + +where.statement === "SELECT id, name, email FROM user WHERE email = ?" +``` + +The following is a list of conditions that can be used in the `where` method. + +| Conditions | Statement | +|----------------------------------------|---------------------------------------| +| `===` | `column = ?` | +| `>=` | `column >= ?` | +| `>` | `column > ?` | +| `<=` | `column <= ?` | +| `<` | `column < ?` | +| `<>` | `column <> ?` | +| `!==` | `column != ?` | +| `IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL")` | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | +| `<=>` | `column <=> ?` | +| `IN (value, value, ...)` | `column IN (?, ?, ...)` | +| `BETWEEN (start, end)` | `column BETWEEN ? AND ?` | +| `LIKE (value)` | `column LIKE ?` | +| `LIKE_ESCAPE (like, escape)` | `column LIKE ? ESCAPE ?` | +| `REGEXP (value)` | `column REGEXP ?` | +| `<<` (value) | `column << ?` | +| `>>` (value) | `column >> ?` | +| `DIV (cond, result)` | `column DIV ? = ?` | +| `MOD (cond, result)` | `column MOD ? = ?` | +| `^ (value)` | `column ^ ?` | +| `~ (value)` | `~column = ?` | + +### GROUP BY/Having + +A type-safe way to set the GROUP BY clause in a query is to use the `groupBy` method. + +Using `groupBy` allows you to group data by the value of a column name you specify when you retrieve the data with `select`. + +```scala +val select = Table[User] + .select(user => (user.id, user.name)) + .groupBy(_._2) + +select.statement === "SELECT id, name FROM user GROUP BY name" +``` + +When grouping, the number of data that can be retrieved with `select` is the number of groups. So, when grouping is done, you can retrieve the values of the columns specified for grouping, or the results of aggregating the column values by group using the provided functions. + +The `having` function allows you to set the conditions under which the data retrieved from the grouping by `groupBy` will be retrieved. + +```scala +val select = Table[User] + .select(user => (user.id, user.name)) + .groupBy(_._2) + .having(_._1 > 1) + +select.statement === "SELECT id, name FROM user GROUP BY name HAVING id > ?" +``` + +### ORDER BY + +A type-safe way to set the ORDER BY clause in a query is to use the `orderBy` method. + +Using `orderBy` allows you to sort data in ascending or descending order by the value of a column name you specify when retrieving data with `select`. + +```scala +val select = Table[User] + .select(user => (user.id, user.name)) + .orderBy(_.id) + +select.statement === "SELECT id, name FROM user ORDER BY id" +``` + +If you want to specify ascending/descending order, simply call `asc`/`desc` for the columns, respectively. + +```scala +val select = Table[User] + .select(user => (user.id, user.name)) + .orderBy(_.id.asc) + +select.statement === "SELECT id, name FROM user ORDER BY id ASC" +``` + +### LIMIT/OFFSET + +A type-safe way to set the LIMIT and OFFSET clauses in a query is to use the `limit`/`offset` methods. + +Setting `limit` allows you to set an upper limit on the number of rows of data to retrieve when `select` is executed, while setting `offset` allows you to specify the number of rows of data to retrieve. + +```scala +val select = Table[User] + .select(user => (user.id, user.name)) + .limit(1) + .offset(1) + +select.statement === "SELECT id, name FROM user LIMIT ? OFFSET ?" +``` + +## JOIN/LEFT JOIN/RIGHT JOIN + +A type-safe way to set a Join in a query is to use the `join`/`leftJoin`/`rightJoin` methods. + +The following definition is used as a sample for Join. + +```scala 3 +case class User(id: Int, name: String, email: String) derives Table +case class Product(id: Int, name: String, price: BigDecimal) derives Table +case class Order( + id: Int, + userId: Int, + productId: Int, + orderDate: LocalDateTime, + quantity: Int +) derives Table + +val userTable = Table[User] +val productTable = Table[Product] +val orderTable = Table[Order] +``` + +First, if you want to perform a simple join, use `join`. +The first argument of `join` is the table to be joined, and the second argument is a function that compares the source table with the columns of the table to be joined. This corresponds to the ON clause in the join. + +After the join, the `select` will specify columns from the two tables. + +```scala +val join = userTable.join(orderTable)((user, order) => user.id === order.userId) + .select((user, order) => (user.name, order.quantity)) + +join.statement = "SELECT user.`name`, order.`quantity` FROM user JOIN order ON user.id = order.user_id" +``` + +Next, if you want to perform a Left Join, which is a left outer join, use `leftJoin`. +The implementation itself is the same as for a simple Join, only `join` is changed to `leftJoin`. + +```scala 3 +val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) + .select((user, order) => (user.name, order.quantity)) + +join.statement = "SELECT user.`name`, order.`quantity` FROM user LEFT JOIN order ON user.id = order.user_id" +``` + +The difference from a simple Join is that when using `leftJoin`, the records retrieved from the table to be joined may be NULL. + +Therefore, in ldbc, all records in the column retrieved from the table passed to `leftJoin` will be of type Option. + +```scala 3 +val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) + .select((user, order) => (user.name, order.quantity)) // (String, Option[Int]) +``` + +Next, if you want to perform a right join, which is a right outer join, use `rightJoin`. +The implementation itself is the same as for a simple join, only the `join` is changed to `rightJoin`. + +```scala 3 +val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) + .select((order, user) => (order.quantity, user.name)) + +join.statement = "SELECT order.`quantity`, user.`name` FROM order RIGHT JOIN user ON order.user_id = user.id" +``` + +The difference from a simple Join is that when using `rightJoin`, the records retrieved from the join source table may be NULL. + +Therefore, in ldbc, all records in the columns retrieved from the join source table using `rightJoin` will be of type Option. + +```scala 3 +val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) + .select((order, user) => (order.quantity, user.name)) // (Option[Int], String) +``` + +If multiple joins are desired, this can be accomplished by calling any Join method in the method chain. + +```scala 3 +val join = + (productTable join orderTable)((product, order) => product.id === order.productId) + .rightJoin(userTable)((_, order, user) => order.userId === user.id) + .select((product, order, user) => (product.name, order.quantity, user.name)) // (Option[String], Option[Int], String)] + +join.statement = + """ + |SELECT + | product.`name`, + | order.`quantity`, + | user.`name` + |FROM product + |JOIN order ON product.id = order.product_id + |RIGHT JOIN user ON order.user_id = user.id + |""".stripMargin +``` + +Note that a `rightJoin` join with multiple joins will result in NULL-acceptable access to all records retrieved from the previously joined table, regardless of what the previous join was. + +## INSERT + +A type-safe way to construct an INSERT statement is to use the following methods provided by Table. + +- insert +- insertInto +- += +- ++= + +**insert** + +The `insert` method is passed a tuple of data to insert. The tuples must have the same number and type of properties as the model. Also, the order of the inserted data must be in the same order as the model properties and table columns. + +```scala 3 +val insert = user.insert((1, "name", "email@example.com")) + +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" +``` + +If you want to insert multiple data, you can construct it by passing multiple tuples to the `insert` method. + +```scala 3 +val insert = user.insert((1, "name 1", "email+1@example.com"), (2, "name 2", "email+2@example.com")) + +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +**insertInto** + +The `insert` method will insert data into all columns the table has, but if you want to insert data only into specific columns, use the `insertInto` method. + +This can be used to exclude data insertion into columns with AutoIncrement or Default values. + +```scala 3 +val insert = user.insertInto(user => (user.name, user.email)).values(("name 3", "email+3@example.com")) + +insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?)" +``` + +If you want to insert multiple data, you can construct it by passing an array of tuples to `values`. + +```scala 3 +val insert = user.insertInto(user => (user.name, user.email)).values(List(("name 4", "email+4@example.com"), ("name 5", "email+5@example.com"))) + +insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?), (?, ?)" +``` + +**+=** + +The `+=` method can be used to construct an INSERT statement using a model. Note that when using a model, data is inserted into all columns. + +```scala 3 +val insert = user += User(6, "name 6", "email+6@example.com") + +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" +``` + +**++=** + +Use the `++=` method if you want to insert multiple data using the model. + +```scala 3 +val insert = user ++= List(User(7, "name 7", "email+7@example.com"), User(8, "name 8", "email+8@example.com")) + +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +### ON DUPLICATE KEY UPDATE + +Inserting a row with an ON DUPLICATE KEY UPDATE clause will cause an UPDATE of the old row if it has a duplicate value in a UNIQUE index or PRIMARY KEY. + +The ldbc way to accomplish this is to use `onDuplicateKeyUpdate` for `Insert`. + +```scala +val insert = user.insert((9, "name", "email+9@example.com")).onDuplicateKeyUpdate(v => (v.name, v.email)) + +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `email` = new_user.`email`" +``` + +## UPDATE + +A type-safe way to construct an UPDATE statement is to use the `update` method provided by Table. + +The first argument of the `update` method is the name of the model property, not the column name of the table, and the second argument is the value to be updated. The type of the value passed as the second argument must be the same as the type of the property specified in the first argument. + +```scala +val update = user.update("name", "update name") + +update.statement === "UPDATE user SET name = ?" +``` + +If a property name that does not exist is specified as the first argument, a compile error occurs. + +```scala 3 +val update = user.update("hoge", "update name") // Compile error +``` + +If you want to update multiple columns, use the `set` method. + +```scala 3 +val update = user.update("name", "update name").set("email", "update-email@example.com") + +update.statement === "UPDATE user SET name = ?, email = ?" +``` + +You can also prevent the `set` method from generating queries based on conditions. + +```scala 3 +val update = user.update("name", "update name").set("email", "update-email@example.com", false) + +update.statement === "UPDATE user SET name = ?" +``` + +You can also use a model to construct the UPDATE statement. Note that if you use a model, all columns will be updated. + +```scala 3 +val update = user.update(User(1, "update name", "update-email@example.com")) + +update.statement === "UPDATE user SET id = ?, name = ?, email = ?" +``` + +## DELETE + +A type-safe way to construct a DELETE statement is to use the `delete` method provided by Table. + +```scala +val delete = user.delete + +delete.statement === "DELETE FROM user" +``` From 345ac4fc330718e460ea70a3c874e232cb8071a1 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:29:14 +0900 Subject: [PATCH 125/160] Create Schema document for English --- docs/src/main/mdoc/en/tutorial/Schema.md | 485 +++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Schema.md diff --git a/docs/src/main/mdoc/en/tutorial/Schema.md b/docs/src/main/mdoc/en/tutorial/Schema.md new file mode 100644 index 000000000..e9b51d31b --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Schema.md @@ -0,0 +1,485 @@ +{% + laika.title = Schema + laika.metadata.language = en +%} + +# Schema + +This chapter describes how to work with database schemas in Scala code, especially how to manually write a schema, which is useful when starting to write an application without an existing database. If you already have a schema in your database, you can skip this step using Code Generator. + +The following dependencies must be set up for your project + +```scala +//> using dep "@ORGANIZATION@::ldbc-schema:@VERSION@" +``` + +The following code example assumes the following import + +```scala 3 +import ldbc.schema.* +import ldbc.schema.attribute.* +``` + +ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in the order of definition. Table definitions are very similar to the structure of a Create statement. This makes the construction of table definitions intuitive for the user. + +ldbc uses these table definitions for a variety of purposes. Generating type-safe queries, generating documents, etc. + +```scala 3 +case class User( + id: Int, + name: String, + email: String, +) + +val table = Table[User]("user")( // CREATE TABLE `user` ( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), // `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(50)), // `name` VARCHAR(50) NOT NULL, + column("email", VARCHAR(100)), // `email` VARCHAR(100) NOT NULL, +) // ); +``` + +All columns are defined by the column method. Each column has a column name, data type, and attributes. The following primitive types are supported by default and are ready to use + +- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` +- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` +- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` +- String +- Boolean +- java.time.* + +Nullable columns are represented by `Option[T]`, where T is one of the supported primitive types; note that any column that is not of type Option is Not Null. + +## Data Type + +The mapping of the Scala type of property that a model has to the data type that a column has requires that the defined data type supports the Scala type. Attempting to assign an unsupported type will result in a compile error. + +The Scala types supported by the data type are listed in the table below. + +| Data Type | Scala Type | +|--------------|-------------------------------------------------------------------------------------------------| +| `BIT` | `Byte, Short, Int, Long` | +| `TINYINT` | `Byte, Short` | +| `SMALLINT` | `Short, Int` | +| `MEDIUMINT` | `Int` | +| `INT` | `Int, Long` | +| `BIGINT` | `Long, BigInt` | +| `DECIMAL` | `BigDecimal` | +| `FLOAT` | `Float` | +| `DOUBLE` | `Double` | +| `CHAR` | `String` | +| `VARCHAR` | `String` | +| `BINARY` | `Array[Byte]` | +| `VARBINARY` | `Array[Byte]` | +| `TINYBLOB` | `Array[Byte]` | +| `BLOB` | `Array[Byte]` | +| `MEDIUMBLOB` | `Array[Byte]` | +| `LONGBLOB` | `Array[Byte]` | +| `TINYTEXT` | `String` | +| `TEXT` | `String` | +| `MEDIUMTEXT` | `String` | +| `DATE` | `java.time.LocalDate` | +| `DATETIME` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime` | +| `TIMESTAMP` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime` | +| `TIME` | `java.time.LocalTime` | +| `YEAR` | `java.time.Instant, java.time.LocalDate, java.time.Year` | +| `BOOLEA` | `Boolean` | + +**Note on working with integer types** + +It should be noted that the range of data that can be handled, depending on whether it is signed or unsigned, does not fit within the Scala types. + +| Data Type | Signed Range | Unsigned Range | Scala Type | Range | +|-------------|----------------------------------------------|----------------------------|------------------|----------------------------------------------------------------------| +| `TINYINT` | `-128 ~ 127` | `0 ~ 255` | `Byte
Short` | `-128 ~ 127
-32768~32767` | +| `SMALLINT` | `-32768 ~ 32767` | `0 ~ 65535` | `Short
Int` | `-32768~32767
-2147483648~2147483647` | +| `MEDIUMINT` | `-8388608 ~ 8388607` | `0 ~ 16777215` | `Int` | `-2147483648~2147483647` | +| `INT` | `-2147483648 ~ 2147483647` | `0 ~ 4294967295` | `Int
Long` | `-2147483648~2147483647
-9223372036854775808~9223372036854775807` | +| `BIGINT` | `-9223372036854775808 ~ 9223372036854775807` | `0 ~ 18446744073709551615` | `Long
BigInt` | `-9223372036854775808~9223372036854775807
...` | + +To work with user-defined proprietary or unsupported types, see Custom Data Types. + +## Attributes + +Columns can be assigned various attributes. + +- `AUTO_INCREMENT`. + Create a DDL statement to mark a column as an auto-increment key when documenting SchemaSPY. + MySQL cannot return columns that are not AutoInc when inserting data. Therefore, if necessary, ldbc will check to see if the return column is properly marked as AutoInc. +- `PRIMARY_KEY`. + Mark the column as a primary key when creating DDL statements or SchemaSPY documents. +- `UNIQUE_KEY`. + Marks a column as a unique key when creating a DDL statement or SchemaSPY document. +- `COMMENT`. + Marks a column as a comment when creating a DDL statement or SchemaSPY document. + +## Setting Keys + +MySQL allows you to set various keys for your tables, such as Unique keys, Index keys, foreign keys, etc. Let's look at how to set these keys in a table definition built with ldbc. + +### PRIMARY KEY + +A primary key is an item that uniquely identifies data in MySQL. When a column has a primary key constraint, it can only contain values that do not duplicate the values of other data. It also cannot contain NULLs. As a result, only one piece of data in the table can be identified by looking up the value of a column with a primary key constraint set. + +In ldbc, this primary key constraint can be set in two ways. + +1. set as an attribute of column method +2. set by keySet method of table + +**Setting as an attribute of the column method**. + +It is very easy to set a column method attribute by simply passing `PRIMARY_KEY` as the third or later argument of the column method. This allows you to set the `id` column as the primary key in the following cases. + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) +``` + +Set by keySet method of **table**. + +ldbc table definitions have a method called `keySet`, where you can set the column you want to set as the primary key by passing `PRIMARY_KEY` as the column name. + +```scala 3 +val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + .keySet(table => PRIMARY_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`) +// ) +``` + +The `PRIMARY_KEY` method can be set to the following parameters in addition to the columns. + +- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH +- `Index Option` ldbc.schema.Index.IndexOption + +#### composite key (primary key) + +Not only one column, but also multiple columns can be combined as a primary key. You can set up a composite primary key by simply passing `PRIMARY_KEY` with the columns you want as primary keys. + +```scala 3 +val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + .keySet(table => PRIMARY_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`, `name`) +// ) +``` + +Compound keys can only be set with `PRIMARY_KEY` in the `keySet` method. If you set multiple attributes in the column method as shown below, each attribute will be set as a primary key, not as a compound key. + +In ldbc, setting multiple `PRIMARY_KEY`s in a table definition does not cause a compile error. However, if the table definition is used in query generation, document generation, etc., an error will occur. This is due to the restriction that only one PRIMARY KEY can be set per table. + +```scala 3 +val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50), PRIMARY_KEY), + column("email", VARCHAR(100)) +) + +// CREATE TABLE `user` ( +// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, +// ) +``` + +### UNIQUE KEY + +A unique key is an item that uniquely identifies data in MySQL. When a uniqueness constraint is set on a column, the column can only contain values that do not duplicate the values of other data. + +In ldbc, this uniqueness constraint can be set in two ways. 1. + +1. as an attribute of the column method +2. in the keySet method of table + +**Setting as an attribute of the column method**. + +It is very easy to set a column method as an attribute by simply passing `UNIQUE_KEY` as the third or later argument of the column method. This allows you to set the `id` column as a unique key in the following cases. + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) +``` + +**Set by keySet method of table** + +The ldbc table definition has a method called `keySet` where you can set a column as a unique key by passing `UNIQUE_KEY` as the column name you want to set as a unique key. + +```scala 3 +val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + .keySet(table => UNIQUE_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`) +// ) +``` + +The `UNIQUE_KEY` method accepts the following parameters in addition to the columns. + +- `Index Name` String +- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH +- `Index Option` ldbc.schema.Index.IndexOption + +#### composite key (unique key) + +You can set not only one column as a unique key, but also multiple columns as a combined unique key. You can set up a composite unique key by simply passing `UNIQUE_KEY` with multiple columns that you want to set as unique keys. + +```scala 3 +val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + .keySet(table => UNIQUE_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`, `name`) +// ) +``` + +Compound keys can only be set with `UNIQUE_KEY` in the `keySet` method. If you set multiple keys as attributes in the column method, each will be set as a unique key, not as a compound key. + +### INDEX KEY + +An index key is an “index” in MySQL to efficiently retrieve the desired record. + +In ldbc, this index can be set in two ways. + +1. as an attribute of the column method +2. by using the keySet method of table. + +**Set as an attribute of the column method** + +It is very easy to set a column method as an attribute, just pass `INDEX_KEY` as the third argument or later of the column method. This allows you to set the `id` column as an index in the following cases + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) +``` + +**Set by keySet method of table** + +The ldbc table definition has a method called `keySet`, where you can set a column as an index key by passing the column you want to set as an index to `INDEX_KEY`. + +```scala 3 +val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + .keySet(table => INDEX_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`) +// ) +``` + +The `INDEX_KEY` method accepts the following parameters in addition to the columns. + +- `Index Name` String +- Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH +- `Index Option` ldbc.schema.Index.IndexOption + +#### composite key (index key) + +You can set not only one column but also multiple columns as index keys as a combined index key. You can set up a composite index by simply passing `INDEX_KEY` with multiple columns that you want to set as index keys. + +```scala 3 +val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + .keySet(table => INDEX_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`, `name`) +// ) +``` + +Compound keys can only be set with `INDEX_KEY` in the `keySet` method. If you set multiple columns as attributes of the `column` method, they will each be set as an index key, not as a composite index. + +### FOREIGN KEY + +A foreign key is a data integrity constraint (referential integrity constraint) in MySQL. A column set to a foreign key can only have values that exist in the columns of the referenced table. + +In ldbc, this foreign key constraint can be set by using the keySet method of table. + +```scala 3 +val user = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + ... +) + .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id))) + +// CREATE TABLE `order` ( +// ..., +// FOREIGN KEY (user_id) REFERENCES `user` (id), +// ) +``` + +The `FOREIGN_KEY` method accepts the following parameters in addition to column and reference values. + +- `Index Name` String + +Foreign key constraints can be used to set the behavior of the parent table on delete and update. The `REFERENCE` method provides `onDelete` and `onUpdate` methods that can be used to set these parameters. + +Values that can be set can be obtained from `ldbc.schema.Reference.ReferenceOption`. + +```scala 3 +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + ... +) + .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id).onDelete(Reference.ReferenceOption.RESTRICT))) + +// CREATE TABLE `order` ( +// ..., +// FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT +// ) +``` + +Possible values are + +- `RESTRICT`: deny delete or update operations on the parent table. +- `CASCADE`: delete or update a row from the parent table and automatically delete or update the matching row in the child table. +- `SET_NULL`: deletes or updates a row from the parent table and sets a foreign key column in the child table to NULL. +- `NO_ACTION`: Standard SQL keyword. In MySQL, equivalent to RESTRICT. +- `SET_DEFAULT`: This action is recognized by the MySQL parser, but both InnoDB and NDB will reject table definitions containing an ON DELETE SET DEFAULT or ON UPDATE SET DEFAULT clause. + +#### composite key (foreign key) + +Not only one column, but also multiple columns can be combined as a foreign key. Simply pass multiple columns to `FOREIGN_KEY` to be set as foreign keys as a compound foreign key. + +```scala 3 +val user = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) +) + +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + column("user_email", VARCHAR(100)) + ... +) + .keySet(table => FOREIGN_KEY((table.userId, table.userEmail), REFERENCE(user, (user.id, user.email)))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`user_id`, `user_email`) REFERENCES `user` (`id`, `email`) +// ) +``` + +### constraint name + +MySQL allows you to give arbitrary names to constraints by using CONSTRAINT. The constraint name must be unique per database. + +Since ldbc provides the CONSTRAINT method, you can set constraints such as key constraints by simply passing them to the CONSTRAINT method. + +```scala 3 +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + ... +) + .keySet(table => CONSTRAINT("fk_user_id", FOREIGN_KEY(table.userId, REFERENCE(user, user.id)))) + +// CREATE TABLE `order` ( +// ..., +// CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +// ) +``` + +## Custom data types + +The way to use user-specific or unsupported types is to tell them what type to treat the column data type as; DataType provides a `mapping` method that can be used to set this up as an implicit type conversion. + +```scala 3 +case class User( + id: Int, + name: User.Name, + email: String, +) + +object User: + + case class Name(firstName: String, lastName: String) + + given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] + + val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) + ) +``` + +ldbc does not allow multiple columns to be merged into a single property of the model, since the purpose of ldbc is to provide a one-to-one mapping between model and table, and to type-safe the table definitions in the database. + +Therefore, it is not allowed to have different number of properties in a table definition and in a model. The following implementation will result in a compile error + +```scala 3 +case class User( + id: Int, + name: User.Name, + email: String, +) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT), + column("first_name", VARCHAR(50)), + column("last_name", VARCHAR(50)), + column("email", VARCHAR(100)) + ) +``` + +If you wish to implement the above, please consider the following implementation. + +```scala 3 +case class User( + id: Int, + firstName: String, + lastName: String, + email: String, +): + + val name: User.Name = User.Name(firstName, lastName) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", INT, AUTO_INCREMENT), + column("first_name", VARCHAR(50)), + column("last_name", VARCHAR(50)), + column("email", VARCHAR(100)) + ) +``` From 2882aafab1df5ed0304257f28d8361cb01340e2a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:36:39 +0900 Subject: [PATCH 126/160] Create Schema Code Generation document for English --- .../en/tutorial/Schema-Code-Generation.md | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md diff --git a/docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md b/docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md new file mode 100644 index 000000000..441338a12 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md @@ -0,0 +1,205 @@ +{% + laika.title = Schema code generation + laika.metadata.language = en +%} + +# Schema code generation + +This chapter describes how to automatically generate ldbc table definitions from SQL files. + +The following dependencies must be set up in your project + +```scala 3 +addSbtPlugin("@ORGANIZATION@" % "ldbc-plugin" % "@VERSION@") +``` + +## Generate + +Enable the plugin for the project. + +```sbt +lazy val root = (project in file(".")) + .enablePlugins(Ldbc) +``` + +Specify the SQL file to be analyzed as an array. + +```sbt +Compile / parseFiles := List(baseDirectory.value / "test.sql") +``` + +**List of keys that can be set by enabling the plugin** + +| Key | Detail | +|----------------------|------------------------------------------------------------------------| +| `parseFiles` | `List of SQL files to be analyzed` | +| `parseDirectories` | `Specify SQL files to be parsed by directory` | +| `excludeFiles` | `List of file names to exclude from analysis` | +| `customYamlFiles` | `List of yaml files for customizing Scala types and column data types` | +| `classNameFormat` | `Value specifying the format of the class name` | +| `propertyNameFormat` | `Value specifying the format of the property name in the Scala model` | +| `ldbcPackage` | `Value specifying the package name of the generated file` | + +The SQL file to be parsed must always begin with a Create or Use statement for the database. ldbc parses the file one file at a time, generates table definitions, and stores the list of tables in the database model. +This is because it is necessary to tell which database the table belongs to. + +```sql +CREATE DATABASE `location`; + +USE `location`; + +DROP TABLE IF EXISTS `country`; +CREATE TABLE country ( + `id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `code` INT NOT NULL +); +``` + +The SQL file to be analyzed should contain only Create/Use statements for the database or Create/Drop statements for table definitions. + +## Generated Code + +When the sbt project is started and compiled, model classes generated based on the SQL file to be analyzed and table definitions are generated under the target of the sbt project. + +```shell +sbt compile +``` + +The code generated from the above SQL file will look like this + +```scala 3 +package ldbc.generated.location + +import ldbc.schema.* + +case class Country( + id: Long, + name: String, + code: Int +) + +object Country: + val table = Table[Country]("country")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("code", INT) + ) +``` + +If the SQL file has been modified or the cache has been removed by running the clean command, Compile will generate the code again. If the SQL file has been modified or the cache has been deleted by executing the clean command, the code will be generated again when Compile is executed. +If you want to generate the code again without using the cache, execute the command `generateBySchema`. This command does not use the cache and always generates code. + +```shell +sbt generateBySchema +``` + +## Customization + +There may be times when you want to convert the type of code generated from an SQL file to something else. This can be done by passing `customYamlFiles` with the yml files to be customized. + +```sbt +Compile / customYamlFiles := List( + baseDirectory.value / "custom.yml" +) +``` + +The format of the yml file should be as follows + +```yaml +database: + name: '{Database Name}' + tables: + - name: '{Table Name}' + columns: # Optional + - name: '{Column Name}' + type: '{Scala type you want to change}' + class: # Optional + extends: + - '{Package paths such as trait that you want model classes to inherit}' // package.trait.name + object: # Optional + extends: + - '{The package path, such as trait, that you want the object to inherit.}' + - name: '{Table Name}' + ... +``` + +The `database` must be the name of the database listed in the SQL file to be analyzed. The table name must be the name of a table belonging to the database listed in the SQL file to be parsed. + +The `columns` field should be a string containing the name of the column whose type you want to change and the Scala type you want to change. Columns` can have multiple values, but the column name in `name` must belong to the target table. +Also, the Scala type to be converted must be one that is supported by the column's Data type. If you want to specify an unsupported type, you must pass a trait, abstract class, etc. that is configured to do implicit type conversion for `object`. + +See [here](/en/tutorial/Schema.md#data-type) for types supported by Data type and [here](/en/tutorial/Schema.md#custom-data-types) for how to set unsupported types. + +To convert an Int type to the user's own type, CountryCode, implement the following `CustomMapping`trait + +```scala 3 +trait CountryCode: + val code: Int +object Japan extends CountryCode: + override val code: Int = 1 + +trait CustomMapping: // 任意の名前 + given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode] +``` + +Set the `CustomMapping`trait that you have implemented in the yml file for customization, and convert the target column type to CountryCode. + +```yaml +database: + name: 'location' + tables: + - name: 'country' + columns: + - name: 'code' + type: 'Country.CountryCode' // CustomMapping is mixed in with the Country object so that it can be retrieved from there. + object: + extends: + - '{package.name.}CustomMapping' +``` + +The code generated by the above configuration will be as follows, allowing users to generate model and table definitions with their own types. + +```scala 3 +case class Country( + id: Long, + name: String, + code: Country.CountryCode +) + +object Country extends /*{package.name.}*/CustomMapping: + val table = Table[Country]("country")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("code", INT) + ) +``` + +The database model is also automatically generated from SQL files. + +```scala 3 +package ldbc.generated.location + +import ldbc.schema.* + +case class LocationDatabase( + schemaMeta: Option[String] = None, + catalog: Option[String] = Some("def"), + host: String = "127.0.0.1", + port: Int = 3306 +) extends Database: + + override val databaseType: Database.Type = Database.Type.MySQL + + override val name: String = "location" + + override val schema: String = "location" + + override val character: Option[Character] = None + + override val collate: Option[Collate] = None + + override val tables = Set( + Country.table + ) +``` From d17574b7fd914d0b34df6d2401feae20c8bed4c3 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:36:54 +0900 Subject: [PATCH 127/160] Create tutorial directory document for English --- docs/src/main/mdoc/en/tutorial/directory.conf | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/src/main/mdoc/en/tutorial/directory.conf b/docs/src/main/mdoc/en/tutorial/directory.conf index e7bc66869..a95cc9a92 100644 --- a/docs/src/main/mdoc/en/tutorial/directory.conf +++ b/docs/src/main/mdoc/en/tutorial/directory.conf @@ -1,4 +1,17 @@ laika.title = Tutorial laika.navigationOrder = [ - index.md + index.md, + Setup.md, + Connection.md, + Simple-Program.md, + Database-Operations.md, + Parameterized-Queries.md, + Selecting-Data.md, + Updating-Data.md, + Error-Handling.md, + Logging.md, + Custom-Data-Type.md, + Query-Builder.md, + Schema.md, + Schema-Code-Generation.md ] From c859e44f59358b6415194740bea7827e3dbb2a63 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 5 Oct 2024 20:57:02 +0900 Subject: [PATCH 128/160] Create Connector document for English --- docs/src/main/mdoc/en/reference/Connector.md | 842 ++++++++++++++++++ .../src/main/mdoc/en/reference/directory.conf | 3 +- 2 files changed, 844 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/mdoc/en/reference/Connector.md diff --git a/docs/src/main/mdoc/en/reference/Connector.md b/docs/src/main/mdoc/en/reference/Connector.md new file mode 100644 index 000000000..5eaf4c8bc --- /dev/null +++ b/docs/src/main/mdoc/en/reference/Connector.md @@ -0,0 +1,842 @@ +{% + laika.title = Connector + laika.metadata.language = en +%} + +# Connector + +This chapter describes database connections using ldbc's own MySQL connector. + +To connect to a MySQL database in Scala, you need to use JDBC, which is a standard Java API that can also be used in Scala. +Since JDBC is implemented in Java, it can only be used in the JVM environment when used in Scala. + +The recent environment surrounding Scala has seen a lot of plugin development to enable Scala to run in JS, Native, and other environments. +Scala has evolved from a JVM-only language that can use Java assets to a language that can run on multiple platforms. + +However, JDBC is a standard Java API that does not support Scala's multi-platform behavior. + +Therefore, even if you create an application in Scala to work with JS, Native, etc., you will not be able to connect to MySQL or other databases because you cannot use JDBC. + +The Typelevel Project has a Scala library for [PostgreSQL](https://www.postgresql.org/) called [Skunk](https://github.com/typelevel/skunk). +This project does not use JDBC and uses pure Scala only to connect to PostgreSQL. Therefore, Skunk can be used to connect to PostgreSQL in any JVM, JS, or Native environment. + +The ldbc connector is a Skunk-inspired project that is being developed to enable connections to MySQL in any JVM, JS, or Native environment. + +※ This connector is currently an experimental feature. Therefore, it should not be used in a production environment. + +The ldbc connector is the lowest layer API. +We plan to use this connector to provide higher-layer APIs in the future. We also plan to make it compatible with existing higher-layer APIs. + +To use this connector, the following dependencies must be set up in your project. + +**JVM** + +```scala 3 +libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" +``` + +**JS/Native** + +```scala 3 +libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" +``` + +**Supported Versions** + +The current version supports the following versions of MySQL + +- MySQL 5.7.x +- MySQL 8.x + +The main support is for MySQL 8.x. MySQL 5.7.x is a sub-support. Therefore, be careful when working with MySQL 5.7.x. +We plan to discontinue support for MySQL 5.7.x in the future. + +## Connection + +Connection` is used to connect to MySQL using the ldbc connector. + +In addition, `Connection` can use `Otel4s` to collect telemetry data so that it can be developed with obserbability in mind. +Therefore, when using `Connection`, the `Tracer` of `Otel4s` must be configured. + +It is recommended to use `Tracer.noop` during development or if you do not need telemetry data using tracing. + +```scala 3 +import cats.effect.IO +import org.typelevel.otel4s.trace.Tracer +import ldbc.connector.Connection + +given Tracer[IO] = Tracer.noop[IO] + +val connection = Connection[IO]( + host = "127.0.0.1", + port = 3306, + user = "root", +) +``` + +The following is a list of properties that can be set when constructing a `Connection + +| Property | Type | Use | +|---------------------------|----------------------|--------------------------------------------------------------------------------------------------------------| +| `host` | `String` | `Specify the host for the MySQL server` | +| `port` | `Int` | `Specify the port number of the MySQL server` | +| `user` | `String` | `Specify the user name to log in to the MySQL server.` | +| `password` | `Option[String]` | `Specify the password of the user who will log in to the MySQL server.` | +| `database` | `Option[String]` | `Specify the database name to be used after connecting to the MySQL server` | +| `debug` | `Boolean` | `Outputs a log of the process. Default is false.` | +| `ssl` | `SSL` | `Specifies whether SSL/TLS is used for notifications to and from the MySQL server. The default is SSL.None.` | +| `socketOptions` | `List[SocketOption]` | `Specifies socket options for TCP/UDP sockets.` | +| `readTimeout` | `Duration` | `Specifies the timeout before an attempt is made to connect to the MySQL server. Default is Duration.Inf.` | +| `allowPublicKeyRetrieval` | `Boolean` | `Specifies whether to use the RSA public key when authenticating with the MySQL server. Default is false.` | + +Connection` uses `Resource` to manage resources. Therefore, when connection information is used, the `use` method is used to manage the resource. + +```scala 3 +connection.use { conn => + // コードを記述 +} +``` + +### Authentication + +Authentication in MySQL involves the client sending user information in a phase called LoginRequest when connecting to the MySQL server. The server then looks up the user in the `mysql.user` table to determine which authentication plugin to use. After the authentication plugin is determined, the server calls the plugin to initiate user authentication and sends the results to the client side. In this way, authentication is pluggable in MySQL. + +The authentication plug-ins supported by MySQL are listed on the [official page](https://dev.mysql.com/doc/refman/8.0/ja/authentication-plugins.html). + +ldbc currently supports the following authentication plug-ins + +- Native Pluggable Authentication +- SHA-256 Pluggable Authentication +- SHA-2 Cached Pluggable Authentication + +※ Native pluggable authentication and SHA-256 pluggable authentication are plugins that have been deprecated since MySQL 8.x. It is recommended that you use the SHA-2 pluggable authentication cache unless you have a good reason to do otherwise. + +There is no need to be aware of authentication plug-ins in the ldbc application code. Users simply create a user created with the authentication plugin they wish to use on the MySQL database and then attempt to connect to MySQL using that user in the ldbc application code. +ldbc will internally determine the authentication plugin and use the appropriate authentication plugin to connect to MySQL. + +## Execution + +The following tables are assumed to be used in the subsequent process. + +```sql +CREATE TABLE users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + age INT NULL +); +``` + +### Statement + +`Statement` is an API for executing SQL without dynamic parameters. + +※ Since `Statement` does not use dynamic parameters, there is a risk of SQL injection depending on its usage. Therefore, it is recommended to use `PreparedStatement` when dynamic parameters are used. + +Construct a `Statement` using the `createStatement` method of `Connection`. + +#### Read Query + +Use the `executeQuery` method to execute read-only SQL. + +The value returned by the MySQL server as a result of executing the query is stored in a `ResultSet` and returned as a return value. + +```scala 3 +connection.use { conn => + for + statement <- conn.createStatement() + result <- statement.executeQuery("SELECT * FROM users") + yield + // Processing with ResultSet +} +``` + +#### Write Query + +Use the `executeUpdate` method to execute the SQL to be written. + +The value returned by the MySQL server as a result of executing the query is the number of rows affected. + +```scala 3 +connection.use { conn => + for + statement <- conn.createStatement() + result <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)") + yield +} +``` + +#### Get the value of AUTO_INCREMENT + +If you want to get the value of AUTO_INCREMENT after executing a query using `Statement`, use the method `getGeneratedKeys`. + +The value returned by the MySQL server as a result of executing the query will be the value generated for AUTO_INCREMENT as the return value. + +```scala 3 +connection.use { conn => + for + statement <- conn.createStatement() + _ <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)", Statement.RETURN_GENERATED_KEYS) + gereatedKeys <- statement.getGeneratedKeys() + yield +} +``` + +### Client/Server PreparedStatement + +ldbc provides `PreparedStatement` divided into `Client PreparedStatement` and `Server PreparedStatement`. + +The `Client PreparedStatement` is an API for constructing SQL on the application using dynamic parameters and sending it to the MySQL server. +Therefore, the method of sending queries to the MySQL server is the same as for `Statement`. + +This API is equivalent to JDBC's `PreparedStatement`. + +Use the `Server PreparedStatement` for building queries in the MySQL server, which is more secure. + +The `Server PreparedStatement` allows queries to be reused since the query to be executed and parameters are sent separately. + +When using `Server PreparedStatement`, the query is prepared in advance by the MySQL server. Although the MySQL server uses memory to store them, the queries can be reused, which improves performance. + +However, there is a risk of memory leaks because the pre-prepared queries will continue to use memory until they are freed. + +If you use `Server PreparedStatement`, you must use the `close` method to properly release the query. + +#### Client PreparedStatement + +Construct a `Client PreparedStatement` using the `ClientPreparedStatement` method of `Connection`. + +```scala 3 +connection.use { conn => + for + statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") + ... + yield ... +} +``` + +#### Server PreparedStatement + +Construct a `Server PreparedStatement` using the `Connection` `serverPreparedStatement` method. + +```scala 3 +connection.use { conn => + for + statement <- conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") + ... + yield ... +} +``` + +#### Read Query + +Use the `executeQuery` method to execute read-only SQL. + +The value returned by the MySQL server as a result of executing the query is stored in a `ResultSet` and returned as a return value. + +```scala 3 +connection.use { conn => + for + statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") + _ <- statement.setLong(1, 1) + result <- statement.executeQuery() + yield + // Processing with ResultSet +} +``` + +If you want to use dynamic parameters, use the `setXXX` method to set the parameters. +The `setXXX` method can also use the `Option` type. If `None` is passed, the parameter will be set to NULL. + +The `setXXX` method specifies the index of the parameter and the value of the parameter. + +```scala 3 +statement.setLong(1, 1) +``` + +The following methods are supported in the current version + +| Method | Type | Note | +|-----------------|---------------------------------------|----------------------------------------------------| +| `setNull` | | Set the parameter to NULL | +| `setBoolean` | `Boolean/Option[Boolean]` | | +| `setByte` | `Byte/Option[Byte]` | | +| `setShort` | `Short/Option[Short]` | | +| `setInt` | `Int/Option[Int]` | | +| `setLong` | `Long/Option[Long]` | | +| `setBigInt` | `BigInt/Option[BigInt]` | | +| `setFloat` | `Float/Option[Float]` | | +| `setDouble` | `Double/Option[Double]` | | +| `setBigDecimal` | `BigDecimal/Option[BigDecimal]` | | +| `setString` | `String/Option[String]` | | +| `setBytes` | `Array[Byte]/Option[Array[Byte]]` | | +| `setDate` | `LocalDate/Option[LocalDate]` | Directly handle `java.time` instead of `java.sql`. | +| `setTime` | `LocalTime/Option[LocalTime]` | Directly handle `java.time` instead of `java.sql`. | +| `setTimestamp` | `LocalDateTime/Option[LocalDateTime]` | Directly handle `java.time` instead of `java.sql`. | +| `setYear` | `Year/Option[Year]` | Directly handle `java.time` instead of `java.sql`. | + +#### Write Query + +Use the `executeUpdate` method to execute the SQL to be written. + +The value returned by the MySQL server as a result of executing the query is the number of rows affected. + +```scala 3 +connection.use { conn => + for + statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") + _ <- statement.setString(1, "Alice") + _ <- statement.setInt(2, 20) + result <- statement.executeUpdate() + yield result +} + +``` + +#### Get the value of AUTO_INCREMENT + +To get the value of AUTO_INCREMENT after executing a query, use the `getGeneratedKeys` method. + +The value returned by the MySQL server as a result of executing the query will be the value generated for AUTO_INCREMENT as the return value. + +```scala 3 +connection.use { conn => + for + statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) + _ <- statement.setString(1, "Alice") + _ <- statement.setInt(2, 20) + _ <- statement.executeUpdate() + getGeneratedKeys <- statement.getGeneratedKeys() + yield getGeneratedKeys +} +``` + +### ResultSet + +The `ResultSet` is an API for storing values returned by the MySQL server after query execution. + +There are two ways to retrieve records retrieved by executing SQL from `ResultSet`: using the `next` and `getXXX` methods as in JDBC, or using ldbc's own `decode` method. + +#### next/getXXX + +The `next` method returns `true` if the next record exists, or `false` if the next record does not exist. + +The `getXXX` method is an API for retrieving values from records. + +The `getXXX` method can be used either by specifying the index of the column to be retrieved or by specifying the column name. + +```scala 3 +connection.use { conn => + for + statement <- conn.clientPreparedStatement("SELECT `id`, `name`, `age` FROM users WHERE id = ?") + _ <- statement.setLong(1, 1) + result <- statement.executeQuery() + records <- Monad[IO].whileM(result.next()) { + for + id <- result.getLong(1) + name <- result.getString("name") + age <- result.getInt(3) + yield (id, name, age) + } + yield records +} +``` + +#### decode + +The `decode` method is an API for converting values retrieved from a `ResultSet` to a Scala type. + +The type to be converted is specified using the `*:` operator depending on the number of columns to be retrieved. + +The example shows how to retrieve the id, name, and age columns of the users table, specifying the type of each column. + +```scala 3 +result.decode(bigint *: varchar *: int.opt) +``` + +If you want to get a NULL-allowed column, use the `opt` method to convert it to the `Option` type. +If the record is NULL, it can be retrieved as None. + +The sequence of events from query execution to record retrieval is as follows + +```scala 3 +connection.use { conn => + for + statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") + _ <- statement.setLong(1, 1) + result <- statement.executeQuery() + decodes <- result.decode(bigint *: varchar *: int.opt) + yield decodes +} +``` + +The records retrieved from a `ResultSet` will always be an array. +This is because a query in MySQL may always return multiple records. + +If you want to retrieve a single record, use the `head` or `headOption` method after the `decode` process. + +The following data types are supported in the current version + +| Codec | Data Type | Scala Type | +|---------------|---------------------|------------------| +| `boolean` | `BOOLEAN` | `Boolean` | +| `tinyint` | `TINYINT` | `Byte` | +| `utinyint` | `unsigned TINYINT` | `Short` | +| `smallint` | `SMALLINT` | `Short` | +| `usmallint` | `unsigned SMALLINT` | `Int` | +| `int` | `INT` | `Int` | +| `uint` | `unsigned INT` | `Long` | +| `bigint` | `BIGINT` | `Long` | +| `ubigint` | `unsigned BIGINT` | `BigInt` | +| `float` | `FLOAT` | `Float` | +| `double` | `DOUBLE` | `Double` | +| `decimal` | `DECIMAL` | `BigDecimal` | +| `char` | `CHAR` | `String` | +| `varchar` | `VARCHAR` | `String` | +| `binary` | `BINARY` | `Array[Byte]` | +| `varbinary` | `VARBINARY` | `String` | +| `tinyblob` | `TINYBLOB` | `String` | +| `blob` | `BLOB` | `String` | +| `mediumblob` | `MEDIUMBLOB` | `String` | +| `longblob` | `LONGBLOB` | `String` | +| `tinytext` | `TINYTEXT` | `String` | +| `text` | `TEXT` | `String` | +| `mediumtext` | `MEDIUMTEXT` | `String` | +| `longtext` | `LONGTEXT` | `String` | +| `enum` | `ENUM` | `String` | +| `set` | `SET` | `List[String]` | +| `json` | `JSON` | `String` | +| `date` | `DATE` | `LocalDate` | +| `time` | `TIME` | `LocalTime` | +| `timetz` | `TIME` | `OffsetTime` | +| `datetime` | `DATETIME` | `LocalDateTime` | +| `timestamp` | `TIMESTAMP` | `LocalDateTime` | +| `timestamptz` | `TIMESTAMP` | `OffsetDateTime` | +| `year` | `YEAR` | `Year` | + +※ Currently, it is designed to retrieve values by specifying the MySQL data type, but in the future it may be changed to a more concise Scala type to retrieve values. + +The following data types are not supported + +- GEOMETRY +- POINT +- LINESTRING +- POLYGON +- MULTIPOINT +- MULTILINESTRING +- MULTIPOLYGON +- GEOMETRYCOLLECTION + +## Transaction + +To execute a transaction using `Connection`, use the `setAutoCommit` method in combination with the `commit` and `rollback` methods. + +First, use the `setAutoCommit` method to disable autocommit for a transaction. + +```scala 3 +conn.setAutoCommit(false) +``` + +Use the `commit` method to commit the transaction after some processing. + +```scala 3 +for + statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") + _ <- statement.setString(1, "Alice") + _ <- statement.setInt(2, 20) + result <- statement.executeUpdate() + _ <- conn.commit() +yield +``` + +Or use the `rollback` method to roll back the transaction. + +```scala 3 +for + statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") + _ <- statement.setString(1, "Alice") + _ <- statement.setInt(2, 20) + result <- statement.executeUpdate() + _ <- conn.rollback() +yield +``` + +If transaction autocommit is disabled using the `setAutoCommit` method, rollback will occur automatically when the connection's Resource is released. + +### transaction isolation level + +ldbc allows you to set the transaction isolation level. + +Transaction isolation levels are set using the `setTransactionIsolation` method. + +The following transaction isolation levels are supported in MySQL + +- READ UNCOMMITTED +- READ COMMITTED +- REPEATABLE READ +- SERIALIZABLE + +See [official documentation](https://dev.mysql.com/doc/refman/8.0/ja/innodb-transaction-isolation-levels.html) for more information on transaction isolation levels in MySQL. + +```scala 3 +import ldbc.connector.Connection.TransactionIsolationLevel + +conn.setTransactionIsolation(TransactionIsolationLevel.REPEATABLE_READ) +``` + +Use the `getTransactionIsolation` method to get the currently set transaction isolation level. + +```scala 3 +for + isolationLevel <- conn.getTransactionIsolation() +yield +``` + +### Savepoint + +For more advanced transaction management, the “Savepoint feature” can be used. This allows you to mark a specific point during a database operation so that if something goes wrong, you can rewind the database state back to that point. This is especially useful for complex database operations or when you need to set a safe point in a long transaction. + +**Feature:** + +- Flexible Transaction Management: Use Savepoint to create a “checkpoint” anywhere within a transaction. State can be returned to that point as needed. +- Error Recovery: Save time and increase efficiency by going back to the last safe Savepoint when an error occurs, rather than starting all over. +- Advanced Control: Multiple Savepoints can be configured for more precise transaction control. Developers can easily implement more complex logic and error handling. + +By taking advantage of this feature, your application will be able to achieve more robust and reliable database operations. + +**Savepoint Settings** + +To set a Savepoint, use the `setSavepoint` method. This method allows you to specify a name for the Savepoint. +If you do not specify a name for the Savepoint, the value generated by the UUID will be set as the default name. + +The `getSavepointName` method can be used to obtain the name of the configured Savepoint. + +※ Since autocommit is enabled by default in MySQL, you must disable autocommit when using Savepoint. Otherwise, all operations will be committed each time, and it will not be possible to roll back transactions using Savepoint. + +```scala 3 +for + _ <- conn.setAutoCommit(false) + savepoint <- conn.setSavepoint("savepoint1") +yield savepoint.getSavepointName +``` + +**Rollback of Savepoint** + +To roll back a part of a transaction using Savepoint, rollback is performed by passing Savepoint to the `rollback` method. +If you commit the entire transaction after a partial rollback using Savepoint, the transaction after that Savepoint will not be committed. + +```scala 3 +for + _ <- conn.setAutoCommit(false) + savepoint <- conn.setSavepoint("savepoint1") + _ <- conn.rollback(savepoint) + _ <- conn.commit() +yield +``` + +**Savepoint Release** + +To release a Savepoint, pass the Savepoint to the `releaseSavepoint` method. +After releasing a Savepoint, commit the entire transaction and the transactions after that Savepoint will be committed. + +```scala 3 +for + _ <- conn.setAutoCommit(false) + savepoint <- conn.setSavepoint("savepoint1") + _ <- conn.releaseSavepoint(savepoint) + _ <- conn.commit() +yield +``` + +## Utility Commands + +MySQL has several utility commands. ([see](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase_utility.html)) + +ldbc provides an API to use these commands. + +| Command | Use | Support | +|------------------------|--------------------------------------------------------------------------------------|---------| +| `COM_QUIT` | `Tells the server that the client is requesting the server to close the connection.` | ✅ | +| `COM_INIT_DB` | `Change the default schema for the connection` | ✅ | +| `COM_STATISTICS` | `Obtain an internal status string in readable format.` | ✅ | +| `COM_DEBUG` | `Dump debugging information to the server's standard output` | ❌ | +| `COM_PING` | `Check if the server is alive` | ✅ | +| `COM_CHANGE_USER` | `Change the user of the current connection` | ✅ | +| `COM_RESET_CONNECTION` | `Reset session state` | ✅ | +| `COM_SET_OPTION` | `Set options for the current connection` | ✅ | + +### COM QUIT + +The `COM_QUIT` command is used to tell the server that the client is requesting that the connection be closed. + +In ldbc, the `close` method of `Connection` can be used to close a connection. +Because the `close` method closes the connection, the connection cannot be used in any subsequent process. + +※ The `Connection` uses `Resource` to manage resources. Therefore, there is no need to use the `close` method to release resources. + +```scala 3 +connection.use { conn => + conn.close() +} +``` + +### COM INIT DB + +The `COM_INIT_DB` command is used to change the default schema for the connection. + +In ldbc, the default schema can be changed using the `setSchema` method of `Connection`. + +```scala 3 +connection.use { conn => + conn.setSchema("test") +} +``` + +### COM STATISTICS + +The `COM_STATISTICS` command is used to retrieve internal status strings in readable format. + +In ldbc, you can use the `getStatistics` method of `Connection` to get the internal status string. + +```scala 3 +connection.use { conn => + conn.getStatistics +} +``` + +The statuses that can be obtained are as follows + +- `uptime` : the time since the server was started +- `threads` : number of clients currently connected. +- `questions` : number of queries since the server started +- `slowQueries` : number of slow queries. +- `opens` : number of table opens since the server started. +- `flushTables` : number of tables flushed since the server started. +- `openTables` : number of tables currently open. +- `queriesPerSecondAvg` : average number of queries per second. + +### COM PING + +The `COM_PING` command is used to check if the server is alive. + +In ldbc, you can check if the server is alive using the `isValid` method of `Connection`. +It returns `true` if the server is alive, `false` if not. + +```scala 3 +connection.use { conn => + conn.isValid +} +``` + +### COM CHANGE USER + +The `COM_CHANGE_USER` command is used to change the user of the current connection. +It also resets the following connection states + +- User Variables +- Temporary tables +- Prepared statements +- etc... + +In ldbc, the `changeUser` method of `Connection` can be used to change the user. + +```scala 3 +connection.use { conn => + conn.changeUser("root", "password") +} +``` + +### COM RESET CONNECTION + +`COM_RESET_CONNECTION` is a command to reset the session state. + +`COM_RESET_CONNECTION` is a more lightweight version of `COM_CHANGE_USER`, with almost the same functionality to clean up the session state, but with the following features + +- Do not re-authenticate (no extra client/server exchange to do so). +- Do not close the connection. + +In ldbc, you can reset the session state using the `resetServerState` method of `Connection`. + +```scala 3 +connection.use { conn => + conn.resetServerState +} +``` + +### COM SET OPTION + +`COM_SET_OPTION` is a command to set options for the current connection. + +In ldbc, you can use the `enableMultiQueries` and `disableMultiQueries` methods of `Connection` to set options. + +The `enableMultiQueries` method allows you to run multiple queries at once. +If you use the `disableMultiQueries` method, you will not be able to run multiple queries at once. + +※ It can only be used for batch processing with Insert, Update, and Delete statements; if used with a Select statement, only the results of the first query will be returned. + +```scala 3 +connection.use { conn => + conn.enableMultiQueries *> conn.disableMultiQueries +} +``` + +## Batch commands + +ldbc allows multiple queries to be executed at once using batch commands. +Using batch commands allows multiple queries to be executed at once, thus reducing the number of network round trips. + +To use batch commands, add queries using the `addBatch` method of a `Statement` or `PreparedStatement` and execute the queries using the `executeBatch` method. + +```scala 3 3 +connection.use { conn => + for + statement <- conn.createStatement() + _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") + _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") + result <- statement.executeBatch() + yield result +} +``` + +In the above example, data for `Alice` and `Bob` can be added at once. +The query to be executed would be as follows + +```sql +INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); +``` + +The return value after executing a batch command is an array of the number of rows affected by each query executed. + +In the above example, one row of data for `Alice` is added and one row of data for `Bob` is added, so the return value is `List(1, 1)`. + +After executing the batch command, the queries that have been added so far by the `addBatch` method will be cleared. + +If you want to clear them manually, use the `clearBatch` method to do so. + +```scala 3 +connection.use { conn => + for + statement <- conn.createStatement() + _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") + _ <- statement.clearBatch() + _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") + _ <- statement.executeBatch() + yield +} +``` + +In the above example, the data for `Alice` is not added, but the data for `Bob` is. + +### Difference between Statement and PreparedStatement + +The queries executed by the batch command may differ between a `Statement` and a `PreparedStatement`. + +When an INSERT statement is executed in a batch command using a `Statement`, multiple queries are executed at once. +However, if you run an INSERT statement in a batch command using a `PreparedStatement`, a single query will be executed. + +For example, if you run the following query in a batch command, multiple queries will be executed at once because you are using a `Statement`. + +```scala 3 +connection.use { conn => + for + statement <- conn.createStatement() + _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") + _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") + result <- statement.executeBatch() + yield result +} + +// Query to be executed +// INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); +``` + +However, if the following query is executed in a batch command, one query will be executed because of the use of `PreparedStatement`. + +```scala 3 +connection.use { conn => + for + statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") + _ <- statement.setString(1, "Alice") + _ <- statement.setInt(2, 20) + _ <- statement.addBatch() + _ <- statement.setString(1, "Bob") + _ <- statement.setInt(2, 30) + _ <- statement.addBatch() + result <- statement.executeBatch() + yield result +} + +// Query to be executed +// INSERT INTO users (name, age) VALUES ('Alice', 20), ('Bob', 30); +``` + +This is because if you are using `PreparedStatement`, you can set multiple parameters for a single query by using the `addBatch` method after setting the query parameters. + +## Stored Procedure Execution + +ldbc provides an API for executing stored procedures. + +To execute a stored procedure, use the `prepareCall` method of `Connection` to construct a `CallableStatement`. + +※ The stored procedures used are those described in the [official](https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-statements-callable.html) document. + +```sql +CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) +BEGIN + DECLARE z INT; + SET z = inOutParam + 1; + SET inOutParam = z; + + SELECT inputParam; + + SELECT CONCAT('zyxw', inputParam); +END +``` + +To execute the above stored procedure, the following would be used + +```scala 3 +connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + hasResult <- callableStatement.execute() + values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { + for + resultSet <- callableStatement.getResultSet().flatMap { + case Some(rs) => IO.pure(rs) + case None => IO.raiseError(new Exception("No result set")) + } + value <- resultSet.getString(1) + yield value + } + yield values // List(Some("abcdefg"), Some("zyxwabcdefg")) +} +``` + +To get the value of an output parameter (a parameter you specified as OUT or INOUT when you created the stored procedure), JDBC requires you to specify the parameter before statement execution using the various `registerOutputParameter()` methods of the CallableStatement interface. to specify parameters before statement execution, while ldbc will also set parameters during query execution by simply setting them using the `setXXX` method. + +However, ldbc also allows you to specify parameters using the `registerOutputParameter()` method. + +```scala 3 +connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) + hasResult <- callableStatement.execute() + value <- callableStatement.getInt(2) + yield value // 2 +} +``` + +※ Note that if you specify an Out parameter with `registerOutParameter`, the value will be set at `Null` for the server if the parameter is not set with the `setXXX` method using the same index value. + +## Unsupported feature + +The ldbc connector is currently an experimental feature. Therefore, the following features are not supported. +We plan to provide the functionality as it becomes available. + +- Connection Pooling +- Failover measures +- etc... diff --git a/docs/src/main/mdoc/en/reference/directory.conf b/docs/src/main/mdoc/en/reference/directory.conf index 721762fb6..104460f08 100644 --- a/docs/src/main/mdoc/en/reference/directory.conf +++ b/docs/src/main/mdoc/en/reference/directory.conf @@ -1,4 +1,5 @@ laika.title = Reference laika.navigationOrder = [ - index.md + index.md, + Connector.md ] From 3e8fbf0987276528c39ad637ebe19bf139357ccf Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 01:41:22 +0900 Subject: [PATCH 129/160] Create LaikaSettings --- project/LaikaSettings.scala | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 project/LaikaSettings.scala diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala new file mode 100644 index 000000000..8a7600818 --- /dev/null +++ b/project/LaikaSettings.scala @@ -0,0 +1,57 @@ +/** + * This file is part of the ldbc. + * For the full copyright and license information, + * please view the LICENSE file that was distributed with this source code. + */ + +import laika.ast.Path.Root +import laika.theme.ThemeProvider +import laika.config.{ Version, Versions } +import laika.helium.config.* + +import org. typelevel. sbt. TypelevelSitePlugin. autoImport.* + +object LaikaSettings { + + object versions { + + val latestRelease = "0.3.0" + + private def version(version: String, label: String = "EOL"): Version = { + val (pathSegment, canonical) = version match { + case "0.3" => ("latest", true) + case _ => (version, false) + } + + val v = + Version(version, pathSegment) + .withFallbackLink("/index.html") + .withLabel(label) + if (canonical) v.setCanonical else v + } + + val v03: Version = version("0.3", "Dev") + val current: Version = v03 + val all: Seq[Version] = Seq(v03) + + val config: Versions = Versions + .forCurrentVersion(current) + .withOlderVersions(all.dropWhile(_ != current).drop(1) *) + .withNewerVersions(all.takeWhile(_ != current) *) + } + + import sbt.* + val helium = Def.setting( + tlSiteHelium.value + .site.internalCSS(Root / "css" / "site.css") + .site.topNavigationBar( + versionMenu = VersionMenu.create( + "Version", + "Choose Version", + additionalLinks = Seq(TextLink.internal(Root / "olderVersions" / "README.md", "Older Versions")) + ) + ) + .site.versions(versions.config) + .build + ) +} From f3bdd68b4b5160660e34180371a3671ac4f6c858 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 01:50:59 +0900 Subject: [PATCH 130/160] Update LaikaSettings --- docs/src/main/mdoc/olderVersions/directory.conf | 4 ++++ docs/src/main/mdoc/olderVersions/index.md | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 docs/src/main/mdoc/olderVersions/directory.conf create mode 100644 docs/src/main/mdoc/olderVersions/index.md diff --git a/docs/src/main/mdoc/olderVersions/directory.conf b/docs/src/main/mdoc/olderVersions/directory.conf new file mode 100644 index 000000000..b3e21b82d --- /dev/null +++ b/docs/src/main/mdoc/olderVersions/directory.conf @@ -0,0 +1,4 @@ +laika.navigationOrder = [ + index.md +] +laika.versioned = false diff --git a/docs/src/main/mdoc/olderVersions/index.md b/docs/src/main/mdoc/olderVersions/index.md new file mode 100644 index 000000000..25d3204ea --- /dev/null +++ b/docs/src/main/mdoc/olderVersions/index.md @@ -0,0 +1,10 @@ +{% + helium.site.pageNavigation.enabled = false + laika.metadata { + language = en + isRootPath = true + } +%} + +Older Versions +============== From 231be14566503a5d8a313e055cdf6ed0968465b2 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 01:51:11 +0900 Subject: [PATCH 131/160] Update LaikaSettings --- project/LaikaSettings.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index 8a7600818..ee8d348ea 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -48,7 +48,7 @@ object LaikaSettings { versionMenu = VersionMenu.create( "Version", "Choose Version", - additionalLinks = Seq(TextLink.internal(Root / "olderVersions" / "README.md", "Older Versions")) + additionalLinks = Seq(TextLink.internal(Root / "olderVersions" / "index.md", "Older Versions")) ) ) .site.versions(versions.config) From 18e4cdc5faa208d2e0473f3b97c21e8071a86a4f Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 01:51:42 +0900 Subject: [PATCH 132/160] Update docs project --- build.sbt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index c285e1516..123e1dd64 100644 --- a/build.sbt +++ b/build.sbt @@ -227,9 +227,7 @@ lazy val docs = (project in file("docs")) "SCALA_VERSION" -> scalaVersion.value, "MYSQL_VERSION" -> mysqlVersion ), - laikaTheme := tlSiteHelium.value.site - .internalCSS(Root / "css" / "site.css") - .build + laikaTheme := LaikaSettings.helium.value ) .settings(commonSettings) .dependsOn( From 6122a9d75542fb9fa6d04c352c828c5e7317124d Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 01:51:54 +0900 Subject: [PATCH 133/160] Change laika.versioned true --- docs/src/main/mdoc/directory.conf | 1 + docs/src/main/mdoc/en/directory.conf | 1 + docs/src/main/mdoc/ja/directory.conf | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/src/main/mdoc/directory.conf b/docs/src/main/mdoc/directory.conf index 9a5a21089..8b7b4d223 100644 --- a/docs/src/main/mdoc/directory.conf +++ b/docs/src/main/mdoc/directory.conf @@ -2,3 +2,4 @@ laika.navigationOrder = [ index.md Migration-Notes.md ] +laika.versioned = true diff --git a/docs/src/main/mdoc/en/directory.conf b/docs/src/main/mdoc/en/directory.conf index 2308d9753..f0c596890 100644 --- a/docs/src/main/mdoc/en/directory.conf +++ b/docs/src/main/mdoc/en/directory.conf @@ -4,3 +4,4 @@ laika.navigationOrder = [ tutorial reference ] +laika.versioned = true diff --git a/docs/src/main/mdoc/ja/directory.conf b/docs/src/main/mdoc/ja/directory.conf index d91d78d7d..2c56f2aa1 100644 --- a/docs/src/main/mdoc/ja/directory.conf +++ b/docs/src/main/mdoc/ja/directory.conf @@ -3,3 +3,4 @@ laika.navigationOrder = [ tutorial reference ] +laika.versioned = true From 4060e63d860f5d8257fa9a8954c1df2d6c0e9067 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 01:53:05 +0900 Subject: [PATCH 134/160] Action sbt scalafmtSbt --- project/LaikaSettings.scala | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index ee8d348ea..10597121c 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -9,7 +9,7 @@ import laika.theme.ThemeProvider import laika.config.{ Version, Versions } import laika.helium.config.* -import org. typelevel. sbt. TypelevelSitePlugin. autoImport.* +import org.typelevel.sbt.TypelevelSitePlugin.autoImport.* object LaikaSettings { @@ -30,9 +30,9 @@ object LaikaSettings { if (canonical) v.setCanonical else v } - val v03: Version = version("0.3", "Dev") - val current: Version = v03 - val all: Seq[Version] = Seq(v03) + val v03: Version = version("0.3", "Dev") + val current: Version = v03 + val all: Seq[Version] = Seq(v03) val config: Versions = Versions .forCurrentVersion(current) @@ -42,16 +42,18 @@ object LaikaSettings { import sbt.* val helium = Def.setting( - tlSiteHelium.value - .site.internalCSS(Root / "css" / "site.css") - .site.topNavigationBar( + tlSiteHelium.value.site + .internalCSS(Root / "css" / "site.css") + .site + .topNavigationBar( versionMenu = VersionMenu.create( "Version", "Choose Version", additionalLinks = Seq(TextLink.internal(Root / "olderVersions" / "index.md", "Older Versions")) ) ) - .site.versions(versions.config) + .site + .versions(versions.config) .build ) } From 2b129c6e52885aa335070f324ad93c59d1765592 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 01:55:16 +0900 Subject: [PATCH 135/160] Delete unused import --- project/LaikaSettings.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index 10597121c..cd9dcaafa 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -4,8 +4,9 @@ * please view the LICENSE file that was distributed with this source code. */ +import sbt.* + import laika.ast.Path.Root -import laika.theme.ThemeProvider import laika.config.{ Version, Versions } import laika.helium.config.* @@ -40,7 +41,6 @@ object LaikaSettings { .withNewerVersions(all.takeWhile(_ != current) *) } - import sbt.* val helium = Def.setting( tlSiteHelium.value.site .internalCSS(Root / "css" / "site.css") From 42021162517e2ef62e15fdb3a9d675f35b383e83 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 20:49:35 +0900 Subject: [PATCH 136/160] Update olderVersions document --- docs/src/main/mdoc/olderVersions/index.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/src/main/mdoc/olderVersions/index.md b/docs/src/main/mdoc/olderVersions/index.md index 25d3204ea..a454f25d1 100644 --- a/docs/src/main/mdoc/olderVersions/index.md +++ b/docs/src/main/mdoc/olderVersions/index.md @@ -8,3 +8,17 @@ Older Versions ============== + +Documentation for versions 0.1 through 0.2 is no longer available on this site, +but you can still browse the documentation sources and scaladoc as linked in the table below. + +It's strongly recommended to upgrade to the latest release (0.3), which has increased functionality and robustness +and now also more API stability than those older releases. + +| Release | Date | Scala Versions | sbt plugin | Scala.js | Scala Native | Doc Sources | API (Scaladoc) | +|---------|----------|----------------|------------|----------|--------------|-------------|-----------------| +| 0.2 | Mar 2024 | 3.3.x | 1.x | | | | [Browse][api02] | +| 0.1 | Dec 2023 | 3.3.x | 1.x | | | | [Browse][api01] | + +[api02]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.2.1/index.html +[api01]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.1.1/index.html From a5d4cb91b8fd3997889482554c4687bf4832ad82 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 20:49:50 +0900 Subject: [PATCH 137/160] Added navLinks settings --- project/LaikaSettings.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index cd9dcaafa..d33827ed1 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -41,11 +41,18 @@ object LaikaSettings { .withNewerVersions(all.takeWhile(_ != current) *) } + private object paths { + val apiLink = "https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/latest/index.html" + } + val helium = Def.setting( tlSiteHelium.value.site .internalCSS(Root / "css" / "site.css") .site .topNavigationBar( + navLinks = Seq( + IconLink.external(paths.apiLink, HeliumIcon.api), + ), versionMenu = VersionMenu.create( "Version", "Choose Version", From b972f16381a4bd23c1a2cf5b8d5455e31c90d108 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Mon, 7 Oct 2024 20:50:25 +0900 Subject: [PATCH 138/160] Action sbt scalafmtSbt --- project/LaikaSettings.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index d33827ed1..31a039c12 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -51,7 +51,7 @@ object LaikaSettings { .site .topNavigationBar( navLinks = Seq( - IconLink.external(paths.apiLink, HeliumIcon.api), + IconLink.external(paths.apiLink, HeliumIcon.api) ), versionMenu = VersionMenu.create( "Version", From a2ccdda18c885c839dedefe97c32865fd08d83c5 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 8 Oct 2024 17:03:27 +0900 Subject: [PATCH 139/160] Create LdbcVersions --- project/Versions.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/project/Versions.scala b/project/Versions.scala index c40034c83..9501e1d97 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -2,6 +2,12 @@ * distributed with this source code. */ +object LdbcVersions { + val latest = "0.3" + val v02 = "0.2" + val v01 = "0.1" +} + object ScalaVersions { val scala2 = "2.12.19" val scala3 = "3.3.4" From 539b111f6207cb3efee87d6313e8b07bc8d91cd2 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 8 Oct 2024 17:03:41 +0900 Subject: [PATCH 140/160] Use LdbcVersions --- build.sbt | 4 +--- project/LaikaSettings.scala | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index 123e1dd64..d6e894991 100644 --- a/build.sbt +++ b/build.sbt @@ -4,8 +4,6 @@ * please view the LICENSE file that was distributed with this source code. */ -import laika.ast.Path.Root - import ScalaVersions.* import JavaVersions.* import BuildSettings.* @@ -14,7 +12,7 @@ import Workflows.* import ProjectKeys.* import Implicits.* -ThisBuild / tlBaseVersion := "0.3" +ThisBuild / tlBaseVersion := LdbcVersions.latest ThisBuild / tlFatalWarnings := true ThisBuild / projectName := "ldbc" ThisBuild / scalaVersion := scala3 diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index 31a039c12..d17e4f76d 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -16,11 +16,9 @@ object LaikaSettings { object versions { - val latestRelease = "0.3.0" - private def version(version: String, label: String = "EOL"): Version = { val (pathSegment, canonical) = version match { - case "0.3" => ("latest", true) + case LdbcVersions.latest => ("latest", true) case _ => (version, false) } @@ -31,7 +29,7 @@ object LaikaSettings { if (canonical) v.setCanonical else v } - val v03: Version = version("0.3", "Dev") + val v03: Version = version("0.3", "Stable") val current: Version = v03 val all: Seq[Version] = Seq(v03) From bce80a7c87c079cf6d12a760d09251ed1a7b22e8 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 8 Oct 2024 17:04:25 +0900 Subject: [PATCH 141/160] Action sbt scalafmtSbt --- project/LaikaSettings.scala | 2 +- project/Versions.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index d17e4f76d..0a179371f 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -19,7 +19,7 @@ object LaikaSettings { private def version(version: String, label: String = "EOL"): Version = { val (pathSegment, canonical) = version match { case LdbcVersions.latest => ("latest", true) - case _ => (version, false) + case _ => (version, false) } val v = diff --git a/project/Versions.scala b/project/Versions.scala index 9501e1d97..5d7db6823 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -4,8 +4,8 @@ object LdbcVersions { val latest = "0.3" - val v02 = "0.2" - val v01 = "0.1" + val v02 = "0.2" + val v01 = "0.1" } object ScalaVersions { From 65219a9ec911354d53184846929d6ff3041463c4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 15 Oct 2024 23:35:49 +0900 Subject: [PATCH 142/160] Action sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfdd631ad..88826dd2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -337,8 +337,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Install sbt - if: contains(runner.os, 'macos') - run: brew install sbt + uses: sbt/setup-sbt@v1 - name: Checkout current branch (full) uses: actions/checkout@v4 From 026d01f82f8164cab065ea736316626fb2e769bc Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 17 Oct 2024 10:06:47 +0900 Subject: [PATCH 143/160] Update template --- docs/src/main/mdoc/default.template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/mdoc/default.template.html b/docs/src/main/mdoc/default.template.html index 1ec929ab9..b96af52f7 100644 --- a/docs/src/main/mdoc/default.template.html +++ b/docs/src/main/mdoc/default.template.html @@ -1,5 +1,5 @@ - + @:include(helium.site.templates.head) From 5555fa1bbec9d0b4959e312fb20ee623f89966cb Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 19 Oct 2024 23:32:14 +0900 Subject: [PATCH 144/160] Change version 0.3 -> 0.2 --- project/LaikaSettings.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index 0a179371f..4efee7abb 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -29,9 +29,9 @@ object LaikaSettings { if (canonical) v.setCanonical else v } - val v03: Version = version("0.3", "Stable") - val current: Version = v03 - val all: Seq[Version] = Seq(v03) + val v02: Version = version("0.2", "Stable") + val current: Version = v02 + val all: Seq[Version] = Seq(v02) val config: Versions = Versions .forCurrentVersion(current) From e34a3dade73f12f7d4731b47edf2a36c765f3881 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 19 Oct 2024 23:34:27 +0900 Subject: [PATCH 145/160] Delete unused --- docs/src/main/mdoc/olderVersions/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/main/mdoc/olderVersions/index.md b/docs/src/main/mdoc/olderVersions/index.md index a454f25d1..fb2582ee6 100644 --- a/docs/src/main/mdoc/olderVersions/index.md +++ b/docs/src/main/mdoc/olderVersions/index.md @@ -17,8 +17,6 @@ and now also more API stability than those older releases. | Release | Date | Scala Versions | sbt plugin | Scala.js | Scala Native | Doc Sources | API (Scaladoc) | |---------|----------|----------------|------------|----------|--------------|-------------|-----------------| -| 0.2 | Mar 2024 | 3.3.x | 1.x | | | | [Browse][api02] | | 0.1 | Dec 2023 | 3.3.x | 1.x | | | | [Browse][api01] | -[api02]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.2.1/index.html [api01]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.1.1/index.html From 2b0107715a93ff4b70f451a50ff3cf252600fdc7 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 19 Oct 2024 23:38:29 +0900 Subject: [PATCH 146/160] Delete unused --- docs/src/main/mdoc/Migration-Notes.md | 333 -------------------------- 1 file changed, 333 deletions(-) delete mode 100644 docs/src/main/mdoc/Migration-Notes.md diff --git a/docs/src/main/mdoc/Migration-Notes.md b/docs/src/main/mdoc/Migration-Notes.md deleted file mode 100644 index 0b8a0c1c2..000000000 --- a/docs/src/main/mdoc/Migration-Notes.md +++ /dev/null @@ -1,333 +0,0 @@ -{% - laika.title = Migration Notes - laika.metadata { - language = en - isRootPath = true - } -%} - -# Migration Notes - -## Upgrading to 0.3.x from 0.2.x - -### Packages - -**Change package name** - -| 0.2.x | 0.3.x | -|-----------|-------------| -| ldbc-core | ldbc-schema | - -**New packages** - -新たに2種類のパッケージが追加されました。 - -| Module / Platform | JVM | Scala Native | Scala.js | -|----------------------|:---:|:------------:|:--------:| -| `ldbc-connector` | ✅ | ✅ | ✅ | -| `jdbc-connector` | ✅ | ❌ | ❌ | - -**全てのパッケージ** - -| Module / Platform | JVM | Scala Native | Scala.js | -|----------------------|:---:|:------------:|:--------:| -| `ldbc-sql` | ✅ | ✅ | ✅ | -| `ldbc-connector` | ✅ | ✅ | ✅ | -| `jdbc-connector` | ✅ | ❌ | ❌ | -| `ldbc-dsl` | ✅ | ✅ | ✅ | -| `ldbc-query-builder` | ✅ | ✅ | ✅ | -| `ldbc-schema` | ✅ | ✅ | ✅ | -| `ldbc-schemaSpy` | ✅ | ❌ | ❌ | -| `ldbc-codegen` | ✅ | ✅ | ✅ | -| `ldbc-hikari` | ✅ | ❌ | ❌ | -| `ldbc-plugin` | ✅ | ❌ | ❌ | - -### 機能変更 - -#### コネクタ切り替え機能 - -Scala MySQL コネクタに、JDBC と ldbc の接続切り替えのサポートが追加されました。 - -この変更により、開発者はプロジェクトの要件に応じて JDBC または ldbc ライブラリを使用したデータベース接続を柔軟に選択できるようになりました。これにより、開発者は異なるライブラリの機能を利用できるようになり、接続の設定や操作の柔軟性が向上します。 - -##### 変更方法 - -まず、共通の依存関係を設定する。 - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@" -``` - -クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ) - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %%% "ldbc-dsl" % "@VERSION@" -``` - -使用される依存パッケージは、データベース接続が Java API を使用するコネクタを介して行われるか、または ldbc によって提供されるコネクタを介して行われるかによって異なります。 - -**jdbcコネクタの使用** - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@" -``` - -**ldbcコネクタの使用** - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" -``` - -クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ) - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" -``` - -##### 使用方法 - -**jdbcコネクタの使用** - -```scala 3 -val ds = new com.mysql.cj.jdbc.MysqlDataSource() -ds.setServerName("127.0.0.1") -ds.setPortNumber(13306) -ds.setDatabaseName("world") -ds.setUser("ldbc") -ds.setPassword("password") - -val datasource = jdbc.connector.MysqlDataSource[IO](ds) - -val connection: Resource[IO, Connection[IO]] = - Resource.make(datasource.getConnection)(_.close()) -``` - -**ldbcコネクタの使用** - -```scala 3 -val connection: Resource[IO, Connection[IO]] = - ldbc.connector.Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "ldbc", - password = Some("password"), - database = Some("ldbc"), - ssl = SSL.Trusted - ) -``` - -データベースへの接続処理は、それぞれの方法で確立されたコネクションを使って行うことができる。 - -```scala 3 -val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn => - (for - result1 <- sql"SELECT 1".query[Int].to[List] - result2 <- sql"SELECT 2".query[Int].to[Option] - result3 <- sql"SELECT 3".query[Int].unsafe - yield (result1, result2, result3)).readOnly(conn) -} -``` - -### 破壊的変更 - -#### プレーン・クエリ構築の拡張 - -プレーン・クエリを用いたデータベース接続メソッドによる検索対象の型の決定は、検索対象の型とそのフォーマット(リストまたはオプション)を一括して指定していた。 - -今回の修正ではこれを変更し、取得する型とその形式の指定を分離することで内部ロジックを共通化した。これにより、プレーン・クエリの構文はよりdoobieに近くなり、doobieのユーザは混乱することなく使用できるはずである。 - -**before** - -```scala 3 -sql"SELECT id, name, age FROM user".toList[(Long, String, Int)].readOnly(connection) -sql"SELECT id, name, age FROM user WHERE id = ${1L}".headOption[User].readOnly(connection) -``` - -**after** - -```scala 3 -sql"SELECT id, name, age FROM user".query[(Long, String, Int)].to[List].readOnly(connection) -sql"SELECT id, name, age FROM user WHERE id = ${1L}".query[User].to[Option].readOnly(connection) -``` - -#### AUTO INCREMENT値取得メソッド命名変更 - -更新 API で AUTO INCREMENT 列によって生成された値を変換する API `updateReturningAutoGeneratedKey` の名前が `returning` に変更されました。 - -これはMySQLの特徴で、MySQLはデータ挿入時にAUTO INCREMENTで生成された値を返しますが、他のRDBは動作が異なり、AUTO INCREMENTで生成された値以外の値を返すことがあります。 -API 名は、将来の拡張を考慮して、限定的な API 名をより拡張しやすくするために早い段階で変更されました。 - -**before** - -```scala 3 -sql"INSERT INTO `table`(`id`, `c1`) VALUES ($None, ${ "column 1" })".updateReturningAutoGeneratedKey[Long] -``` - -**after** - -```scala 3 -sql"INSERT INTO `table`(`id`, `c1`) VALUES ($None, ${ "column 1" })".returning[Long] -``` - -#### クエリビルダーの構築方法 - -以前まではクエリビルダーはテーブルスキーマを構築しなければ使用することができませんでした。 - -今回の更新で、より簡易的にクエリビルダーを使用できるように変更を行いました。 - -**before** - -まずモデルに対応したテーブルスキーマを作成し、 - -```scala 3 -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val userTable = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) -``` - -次にテーブルスキーマを使用して`TableQuery`の構築を行います。 - -```scala 3 -val tableQuery = TableQuery[IO, User](userTable) -``` - -最後にクエリ構築を行っていました。 - -```scala 3 -val result: IO[List[User]] = connection.use { conn => - tableQuery.selectAll.toList[User].readOnly(conn) - // "SELECT `id`, `name`, `age` FROM user" -} -``` - -**after** - -今回の変更によって、モデルを構築し - -```scala 3 -import ldbc.query.builder.Table - -case class User( - id: Long, - name: String, - age: Option[Int], -) derives Table -``` - -次に`Table`を初期化を行います。 - -```scala 3 -import ldbc.query.builder.Table - -val userTable = Table[User] -``` - -最後にクエリ構築を行うことで利用可能となります。 - -```scala -val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query.to[List].readOnly(conn) - // "SELECT `id`, `name`, `age` FROM user" -} -``` - -#### カスタムデータ型のサポート - -ユーザー定義のデータ型を使用する際は、`ResultSetReader`と`Parameter`を使用してカスタムデータ型をサポートしていました。 - -今回の更新で、`ResultSetReader`と`Parameter`を使用してカスタムデータ型をサポートする方法が変更されました。 - -##### Encoder - -クエリ文字列に動的に埋め込むために、`Parameter`から`Encoder`に変更。 - -これにより、ユーザはEffect Typeを受け取るための冗長な処理を記述する必要がなくなり、よりシンプルな実装とカスタムデータ型のパラメータとしての使用が可能になります。 - -```scala -enum Status(val code: Int, val name: String): - case Active extends Status(1, "Active") - case InActive extends Status(2, "InActive") -``` - -**Before** - -```scala -given Parameter[Status] with - override def bind[F[_]]( - statement: PreparedStatement[F], - index: Int, - status: Status - ): F[Unit] = statement.setInt(index, status.code) -``` - -**After** - -```scala -given Encoder[Status] with - override def encode(status: Status): Int = status.done -``` - -`Encoder`のエンコード処理では、`PreparedStatement`で扱えるScala型しか返すことができません。 - -現在、以下のタイプがサポートされている。 - -| Scala Type | Methods called in PreparedStatement | -|---------------------------|-------------------------------------| -| `Boolean` | `setBoolean` | -| `Byte` | `setByte` | -| `Short` | `setShort` | -| `Int` | `setInt` | -| `Long` | `setLong` | -| `Float` | `setFloat` | -| `Double` | `setDouble` | -| `BigDecimal` | `setBigDecimal` | -| `String` | `setString` | -| `Array[Byte]` | `setBytes` | -| `java.time.LocalDate` | `setDate` | -| `java.time.LocalTime` | `setTime` | -| `java.time.LocalDateTime` | `setTimestamp` | -| `None` | `setNull` | - -##### Decoder - -`ResultSet`からデータを取得する処理を`ResultSetReader`から`Decoder`に変更。 - -これにより、ユーザーは取得したレコードをネストした階層データに変換できる。 - - -```scala -case class City(id: Int, name: String, countryCode: String) -case class Country(code: String, name: String) -case class CityWithCountry(city: City, country: Country) - -sql"SELECT city.Id, city.Name, city.CountryCode, country.Code, country.Name FROM city JOIN country ON city.CountryCode = country.Code".query[CityWithCountry] -``` - -**Using Query Builder** - -```scala -case class City(id: Int, name: String, countryCode: String) derives Table -case class Country(code: String, name: String) derives Table - -val city = Table[City] -val country = Table[Country] - -city.join(country).join((city, country) => city.countryCode === country.code) - .select((city, country) => (city.name, country.name)) - .query // (String, String) - .to[Option] - - -city.join(country).join((city, country) => city.countryCode === country.code) - .selectAll - .query // (City, Country) - .to[Option] -``` From 15b8bbe1ef41673fe392940cfd392d90d3a35207 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 19 Oct 2024 23:38:42 +0900 Subject: [PATCH 147/160] Update index.md --- docs/src/main/mdoc/index.md | 205 +----------------------------------- 1 file changed, 4 insertions(+), 201 deletions(-) diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md index 3eca118f3..cd9e83913 100644 --- a/docs/src/main/mdoc/index.md +++ b/docs/src/main/mdoc/index.md @@ -13,215 +13,18 @@ laika.metadata { style = "center-logo" } -[![Continuous Integration](https://github.com/takapi327/ldbc/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/takapi327/ldbc/actions/workflows/ci.yml) +[![Maven Central Version](https://img.shields.io/maven-central/v/io.github.takapi327/ldbc-core_3?color=blue)](https://search.maven.org/artifact/io.github.takapi327/ldbc-core_3/0.2.1/jar) [![MIT License](https://img.shields.io/badge/license-MIT-green)](https://en.wikipedia.org/wiki/MIT_License) [![Scala Version](https://img.shields.io/badge/scala-v3.3.x-red)](https://github.com/lampepfl/dotty) [![Typelevel Affiliate Project](https://img.shields.io/badge/typelevel-affiliate%20project-FF6169.svg)](https://typelevel.org/projects/affiliate/) -[![javadoc](https://javadoc.io/badge2/io.github.takapi327/ldbc-dsl_3/javadoc.svg)](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) -[![Maven Central Version](https://maven-badges.herokuapp.com/maven-central/io.github.takapi327/ldbc-dsl_3/badge.svg?color=blue)](https://search.maven.org/artifact/io.github.takapi327/ldbc-dsl_3/0.3.0-beta8/jar) -[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue)](https://index.scala-lang.org/takapi327/ldbc) -[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue&targetType=js)](https://index.scala-lang.org/takapi327/ldbc) -[![scaladex](https://index.scala-lang.org/takapi327/ldbc/ldbc-dsl/latest-by-scala-version.svg?color=blue&targetType=native)](https://index.scala-lang.org/takapi327/ldbc) - -======================================================================================== ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effect 3 and Scala 3. -ldbc is a [Typelevel](http://typelevel.org/) project. This means we embrace pure, typeful, functional programming, and provide a safe and friendly environment for teaching, learning, and contributing as described in the Scala [Code of Conduct](http://scala-lang.org/conduct.html). - -Note that **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. - -## Modules availability - -ldbc is available on the JVM, Scala.js, and ScalaNative - -| Module / Platform | JVM | Scala Native | Scala.js | -|----------------------|:---:|:------------:|:--------:| -| `ldbc-core` | ✅ | ✅ | ✅ | -| `ldbc-sql` | ✅ | ✅ | ✅ | -| `ldbc-connector` | ✅ | ✅ | ✅ | -| `jdbc-connector` | ✅ | ❌ | ❌ | -| `ldbc-dsl` | ✅ | ✅ | ✅ | -| `ldbc-query-builder` | ✅ | ✅ | ✅ | -| `ldbc-schema` | ✅ | ✅ | ✅ | -| `ldbc-schemaSpy` | ✅ | ❌ | ❌ | -| `ldbc-codegen` | ✅ | ✅ | ✅ | -| `ldbc-hikari` | ✅ | ❌ | ❌ | -| `ldbc-plugin` | ✅ | ❌ | ❌ | - -## Quick Start - -For people that want to skip the explanations and see it action, this is the place to start! - -### Dependency Configuration - -```scala -libraryDependencies += "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@" -``` - -For Cross-Platform projects (JVM, JS, and/or Native): - -```scala -libraryDependencies += "@ORGANIZATION@" %%% "ldbc-dsl" % "@VERSION@" -``` - -The dependency package used depends on whether the database connection is made via a connector using the Java API or a connector provided by ldbc. - -**Use jdbc connector** - -```scala -libraryDependencies += "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@" -``` +ldbc is Created under the influence of [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. Using tapir, you can build type-safe endpoints and also generate OpenAPI documentation from the endpoints you build. -**Use ldbc connector** - -```scala -libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" -``` - -For Cross-Platform projects (JVM, JS, and/or Native) - -```scala -libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" -``` - -### Usage - -The difference in usage is that there are differences in the way connections are built between jdbc and ldbc. - -> **ldbc** is currently under active development. Please note that current functionality may therefore be deprecated or changed in the future. - -**jdbc connector** - -```scala -val ds = new com.mysql.cj.jdbc.MysqlDataSource() -ds.setServerName("127.0.0.1") -ds.setPortNumber(3306) -ds.setDatabaseName("world") -ds.setUser("ldbc") -ds.setPassword("password") - -val datasource = jdbc.connector.MysqlDataSource[IO](ds) - -val connection: Resource[IO, Connection[IO]] = - Resource.make(datasource.getConnection)(_.close()) -``` - -**ldbc connector** - -```scala -val connection: Resource[IO, Connection[IO]] = - ldbc.connector.Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "ldbc", - password = Some("password"), - database = Some("ldbc"), - ssl = SSL.Trusted - ) -``` - -The connection process to the database can be carried out using the connections established by each of these methods. - -```scala -val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn => - (for - result1 <- sql"SELECT 1".toList[Int] - result2 <- sql"SELECT 2".headOption[Int] - result3 <- sql"SELECT 3".unsafe[Int] - yield (result1, result2, result3)).readOnly(conn) -} -``` +ldbc allows the same type-safe construction with Scala at the database layer and document generation using the constructed one. -#### Using the query builder - -ldbc provides not only plain queries but also type-safe database connections using the query builder. - -The first step is to set up dependencies. - -```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-query-builder" % "${version}" -``` - -For Cross-Platform projects (JVM, JS, and/or Native): - -```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-query-builder" % "${version}" -``` - -ldbc uses classes to construct queries. - -```scala -import ldbc.query.builder.Table - -case class User( - id: Long, - name: String, - age: Option[Int], -) derives Table -``` - -The next step is to create a Table using the classes you have created. - -```scala -import ldbc.query.builder.Table - -val userTable = Table[User] -``` - -Finally, you can use the query builder to create a query. - -```scala -val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query.to[List].readOnly(conn) - // "SELECT `id`, `name`, `age` FROM user" -} -``` - -#### Using the schema - -ldbc also allows type-safe construction of schema information for tables. - -The first step is to set up dependencies. - -```scala -libraryDependencies += "@ORGANIZATION@" %% "ldbc-schema" % "@VERSION@" -``` - -For Cross-Platform projects (JVM, JS, and/or Native): - -```scala -libraryDependencies += "@ORGANIZATION@" %%% "ldbc-schema" % "@VERSION@" -``` - -The next step is to create a schema for use by the query builder. - -ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in definition order. Table definitions are very similar to the structure of Create statements. This makes the construction of table definitions intuitive for the user. - -```scala -import ldbc.schema.* - -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val userTable = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) // ) -``` - -Finally, you can use the query builder to create a query. - -```scala -val result: IO[List[User]] = connection.use { conn => - userTable.selectAll.query.to[List].readOnly(conn) - // "SELECT `id`, `name`, `age` FROM user" -} -``` +Note that **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. ## Documentation From d0346157548f3e83eb02e6dd6b9399c592b6aa38 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 19 Oct 2024 23:42:14 +0900 Subject: [PATCH 148/160] Delete unused --- docs/src/main/mdoc/ja/reference/Connector.md | 843 ------------------ .../src/main/mdoc/ja/reference/directory.conf | 5 - docs/src/main/mdoc/ja/reference/index.md | 14 - docs/src/main/mdoc/ja/tutorial/Connection.md | 93 -- .../main/mdoc/ja/tutorial/Custom-Data-Type.md | 68 -- .../mdoc/ja/tutorial/Database-Operations.md | 53 -- .../main/mdoc/ja/tutorial/Error-Handling.md | 37 - docs/src/main/mdoc/ja/tutorial/Logging.md | 52 -- .../mdoc/ja/tutorial/Parameterized-Queries.md | 112 --- .../main/mdoc/ja/tutorial/Query-Builder.md | 392 -------- .../ja/tutorial/Schema-Code-Generation.md | 205 ----- docs/src/main/mdoc/ja/tutorial/Schema.md | 485 ---------- .../main/mdoc/ja/tutorial/Selecting-Data.md | 147 --- docs/src/main/mdoc/ja/tutorial/Setup.md | 183 ---- .../main/mdoc/ja/tutorial/Simple-Program.md | 106 --- .../main/mdoc/ja/tutorial/Updating-Data.md | 115 --- docs/src/main/mdoc/ja/tutorial/directory.conf | 17 - docs/src/main/mdoc/ja/tutorial/index.md | 14 - 18 files changed, 2941 deletions(-) delete mode 100644 docs/src/main/mdoc/ja/reference/Connector.md delete mode 100644 docs/src/main/mdoc/ja/reference/directory.conf delete mode 100644 docs/src/main/mdoc/ja/reference/index.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Connection.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Database-Operations.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Error-Handling.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Logging.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Query-Builder.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Schema-Code-Generation.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Schema.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Selecting-Data.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Setup.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Simple-Program.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/Updating-Data.md delete mode 100644 docs/src/main/mdoc/ja/tutorial/directory.conf delete mode 100644 docs/src/main/mdoc/ja/tutorial/index.md diff --git a/docs/src/main/mdoc/ja/reference/Connector.md b/docs/src/main/mdoc/ja/reference/Connector.md deleted file mode 100644 index 430273981..000000000 --- a/docs/src/main/mdoc/ja/reference/Connector.md +++ /dev/null @@ -1,843 +0,0 @@ -{% - laika.title = コネクタ - laika.metadata.language = ja -%} - -# コネクタ - -この章では、ldbc独自のMySQLコネクタを使用したデータベース接続について説明します。 - -ScalaでMySQLデータベースへの接続を行うためにはJDBCを使用する必要があります。JDBCはJavaの標準APIであり、Scalaでも使用することができます。 -JDBCはJavaで実装が行われているためScalaで使用する場合でもJVM環境でのみ動作することができます。 - -昨今のScalaを取り巻く環境はJSやNativeなどの環境でも動作できるようプラグインの開発が盛んに行われています。 -ScalaはJavaの資産を使用できるJVMのみで動作する言語から、マルチプラットフォーム環境でも動作できるよう進化を続けています。 - -しかし、JDBCはJavaの標準APIでありScalaのマルチプラットフォーム環境での動作をサポートしていません。 - -そのため、ScalaでアプリケーションをJS, Nativeなどで動作できるように作成を行ったとしてもJDBCを使用できないため、MySQLなどのデータベースへ接続を行うことができません。 - -Typelevel Projectには[Skunk](https://github.com/typelevel/skunk)と呼ばれる[PostgreSQL](https://www.postgresql.org/)用のScalaライブラリが存在します。 -このプロジェクトはJDBCを使用しておらず、純粋なScalaのみでPostgreSQLへの接続を実現しています。そのため、Skunkを使用すればJVM, JS, Native環境を問わずPostgreSQLへの接続を行うことができます。 - -ldbc コネクタはこのSkunkに影響を受けてJVM, JS, Native環境を問わずMySQLへの接続を行えるようにするために開発が行われてるプロジェクトです。 - -※ このコネクタは現在実験的な機能となります。そのため本番環境での使用しないでください。 - -ldbcコネクタは一番低レイヤーのAPIとなります。 -今後このコネクタを使用してより高レイヤーのAPIを提供する予定です。また既存の高レイヤーのAPIとの互換性を持たせることも予定しています。 - -使用するにはプロジェクトに以下の依存関係を設定する必要があります。 - -**JVM** - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" -``` - -**JS/Native** - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" -``` - -**サポートバージョン** - -現在のバージョンは以下のバージョンのMySQLをサポートしています。 - -- MySQL 5.7.x -- MySQL 8.x - -メインサポートはMySQL 8.xです。MySQL 5.7.xはサブサポートとなります。そのためMySQL 5.7.xでの動作には注意が必要です。 -将来的にはMySQL 5.7.xのサポートは終了する予定です。 - -## 接続 - -ldbcコネクタを使用してMySQLへの接続を行うためには、`Connection`を使用します。 - -また、`Connection`はオブザーバビリティを意識した開発を行えるように`Otel4s`を使用してテレメトリデータを収集できるようにしています。 -そのため、`Connection`を使用する際には`Otel4s`の`Tracer`を設定する必要があります。 - -開発時やトレースを使用したテレメトリデータが不要な場合は`Tracer.noop`を使用することを推奨します。 - -```scala 3 -import cats.effect.IO -import org.typelevel.otel4s.trace.Tracer -import ldbc.connector.Connection - -given Tracer[IO] = Tracer.noop[IO] - -val connection = Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "root", -) -``` - -以下は`Connection`構築時に設定できるプロパティの一覧です。 - -| プロパティ | 型 | 用途 | -|---------------------------|----------------------|----------------------------------------------------------| -| `host` | `String` | `MySQLサーバーのホストを指定します` | -| `port` | `Int` | `MySQLサーバーのポート番号を指定します` | -| `user` | `String` | `MySQLサーバーへログインを行うユーザー名を指定します` | -| `password` | `Option[String]` | `MySQLサーバーへログインを行うユーザーのパスワードを指定します` | -| `database` | `Option[String]` | `MySQLサーバーへ接続後に使用するデータベース名を指定します` | -| `debug` | `Boolean` | `処理のログを出力します。デフォルトはfalseです` | -| `ssl` | `SSL` | `MySQLサーバーとの通知んでSSL/TLSを使用するかを指定します。デフォルトはSSL.Noneです` | -| `socketOptions` | `List[SocketOption]` | `TCP/UDP ソケットのソケットオプションを指定します。` | -| `readTimeout` | `Duration` | `MySQLサーバーへの接続を試みるまでのタイムアウトを指定します。デフォルトはDuration.Infです。` | -| `allowPublicKeyRetrieval` | `Boolean` | `MySQLサーバーとの認証時にRSA公開鍵を使用するかを指定します。デフォルトはfalseです。` | - -`Connection`は`Resource`を使用してリソース管理を行います。そのためコネクション情報を使用する場合は`use`メソッドを使用してリソースの管理を行います。 - -```scala 3 -connection.use { conn => - // コードを記述 -} -``` - -### 認証 - -MySQLでの認証は、クライアントがMySQLサーバーへ接続するときにLoginRequestというフェーズでユーザ情報を送信します。そして、サーバー側では送られたユーザが`mysql.user`テーブルに存在するか検索を行い、どの認証プラグインを使用するかを決定します。認証プラグインが決定した後にサーバーはそのプラグインを呼び出してユーザー認証を開始し、その結果をクライアント側に送信します。このようにMySQLでは認証がプラガブル(様々なタイプのプラグインを付け外しできる)になっています。 - -MySQLでサポートされている認証プラグインは[公式ページ](https://dev.mysql.com/doc/refman/8.0/ja/authentication-plugins.html)に記載されています。 - -ldbcは現時点で以下の認証プラグインをサポートしています。 - -- ネイティブプラガブル認証 -- SHA-256 プラガブル認証 -- SHA-2 プラガブル認証のキャッシュ - -※ ネイティブプラガブル認証とSHA-256 プラガブル認証はMySQL 8.xから非推奨となったプラグインです。特段理由がない場合はSHA-2 プラガブル認証のキャッシュを使用することを推奨します。 - -ldbcのアプリケーションコード上で認証プラグインを意識する必要はありません。ユーザーはMySQLのデータベース上で使用したい認証プラグインで作成されたユーザーを作成し、ldbcのアプリケーションコード上ではそのユーザーを使用してMySQLへの接続を試みるだけで問題ありません。 -ldbcが内部で認証プラグインを判断し、適切な認証プラグインを使用してMySQLへの接続を行います。 - -## 実行 - -以降の処理では以下テーブルを使用しているものとします。 - -```sql -CREATE TABLE users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - age INT NULL -); -``` - -### Statement - -`Statement`は動的なパラメーターを使用しないSQLを実行するためのAPIです。 - -※ `Statement`は動的なパラメーターを使用しないため、使い方によってはSQLインジェクションのリスクがあります。そのため、動的なパラメーターを使用する場合は`PreparedStatement`を使用することを推奨します。 - -`Connection`の`createStatement`メソッドを使用して`Statement`を構築します。 - -#### 読み取りクエリ - -読み取り専用のSQLを実行する場合は`executeQuery`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は`ResultSet`に格納されて戻り値として返却されます。 - -```scala 3 3 -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeQuery("SELECT * FROM users") - yield - // ResultSetを使用した処理 -} -``` - -#### 書き込みクエリ - -書き込みを行うSQLを実行する場合は`executeUpdate`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は影響を受けた行数が戻り値として返却されます。 - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)") - yield -} -``` - -#### AUTO_INCREMENTの値を取得 - -`Statement`を使用してクエリ実行後にAUTO_INCREMENTの値を取得する場合は`getGeneratedKeys`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値はAUTO_INCREMENTに生成された値が戻り値として返却されます。 - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)", Statement.RETURN_GENERATED_KEYS) - gereatedKeys <- statement.getGeneratedKeys() - yield -} -``` - -### Client/Server PreparedStatement - -ldbcでは`PreparedStatement`を`Client PreparedStatement`と`Server PreparedStatement`に分けて提供しています。 - -`Client PreparedStatement`は動的なパラメーターを使用してアプリケーション上でSQLの構築を行い、MySQLサーバーに送信を行うためのAPIです。 -そのためMySQLサーバーへのクエリ送信方法は`Statement`と同じになります。 - -このAPIはJDBCの`PreparedStatement`に相当します。 - -より安全なMySQLサーバー内でクエリを構築するための`PreparedStatement`は`Server PreparedStatement`で提供されますので、そちらを使用してください。 - -`Server PreparedStatement`は実行を行うクエリをMySQLサーバー内で事前に準備を行い、アプリケーション上でパラメーターを設定して実行を行うためのAPIです。 - -`Server PreparedStatement`では実行するクエリの送信とパラメーターの送信が分けて行われるため、クエリの再利用が可能となります。 - -`Server PreparedStatement`を使用する場合事前にクエリをMySQLサーバーで準備します。格納するためにMySQLサーバーはメモリを使用しますが、クエリの再利用が可能となるため、パフォーマンスの向上が期待できます。 - -しかし、事前準備されたクエリは解放されるまでメモリを使用し続けるため、メモリリークのリスクがあります。 - -`Server PreparedStatement`を使用する場合は`close`メソッドを使用して適切にクエリの解放を行う必要があります。 - -#### Client PreparedStatement - -`Connection`の`clientPreparedStatement`メソッドを使用して`Client PreparedStatement`を構築します。 - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### Server PreparedStatement - -`Connection`の`serverPreparedStatement`メソッドを使用して`Server PreparedStatement`を構築します。 - -```scala 3 -connection.use { conn => - for - statement <- conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### 読み取りクエリ - -読み取り専用のSQLを実行する場合は`executeQuery`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は`ResultSet`に格納されて戻り値として返却されます。 - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - yield - // ResultSetを使用した処理 -} -``` - -動的なパラメーターを使用する場合は`setXXX`メソッドを使用してパラメーターを設定します。 -`setXXX`メソッドは`Option`型を使用することもできます。`None`が渡された場合パラメーターにはNULLがセットされます。 - -`setXXX`メソッドはパラメーターのインデックスとパラメーターの値を指定します。 - -```scala 3 -statement.setLong(1, 1) -``` - -現在のバージョンでは以下のメソッドがサポートされています。 - -| メソッド | 型 | 備考 | -|-----------------|---------------------------------------|-----------------------------------| -| `setNull` | | パラメーターにNULLをセットします | -| `setBoolean` | `Boolean/Option[Boolean]` | | -| `setByte` | `Byte/Option[Byte]` | | -| `setShort` | `Short/Option[Short]` | | -| `setInt` | `Int/Option[Int]` | | -| `setLong` | `Long/Option[Long]` | | -| `setBigInt` | `BigInt/Option[BigInt]` | | -| `setFloat` | `Float/Option[Float]` | | -| `setDouble` | `Double/Option[Double]` | | -| `setBigDecimal` | `BigDecimal/Option[BigDecimal]` | | -| `setString` | `String/Option[String]` | | -| `setBytes` | `Array[Byte]/Option[Array[Byte]]` | | -| `setDate` | `LocalDate/Option[LocalDate]` | `java.sql`ではなく`java.time`を直接扱います。 | -| `setTime` | `LocalTime/Option[LocalTime]` | `java.sql`ではなく`java.time`を直接扱います。 | -| `setTimestamp` | `LocalDateTime/Option[LocalDateTime]` | `java.sql`ではなく`java.time`を直接扱います。 | -| `setYear` | `Year/Option[Year]` | `java.sql`ではなく`java.time`を直接扱います。 | - -#### 書き込みクエリ - -書き込みを行うSQLを実行する場合は`executeUpdate`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値は影響を受けた行数が戻り値として返却されます。 - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - yield result -} - -``` - -#### AUTO_INCREMENTの値を取得 - -クエリ実行後にAUTO_INCREMENTの値を取得する場合は`getGeneratedKeys`メソッドを使用します。 - -クエリを実行した結果MySQLサーバーから返される値はAUTO_INCREMENTに生成された値が戻り値として返却されます。 - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.executeUpdate() - getGeneratedKeys <- statement.getGeneratedKeys() - yield getGeneratedKeys -} -``` - -### ResultSet - -`ResultSet`はクエリ実行後にMySQLサーバーから返された値を格納するためのAPIです。 - -SQLを実行して取得したレコードを`ResultSet`から取得するにはJDBCと同じように`next`メソッドと`getXXX`メソッドを使用して取得する方法と、ldbc独自の`decode`メソッドを使用する方法があります。 - -#### next/getXXX - -`next`メソッドは次のレコードが存在する場合は`true`を返却し、次のレコードが存在しない場合は`false`を返却します。 - -`getXXX`メソッドはレコードから値を取得するためのAPIです。 - -`getXXX`メソッドは取得するカラムのインデックスを指定する方法とカラム名を指定する方法があります。 - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT `id`, `name`, `age` FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - records <- Monad[IO].whileM(result.next()) { - for - id <- result.getLong(1) - name <- result.getString("name") - age <- result.getInt(3) - yield (id, name, age) - } - yield records -} -``` - -#### decode - -`decode`メソッドは`ResultSet`から取得した値をScalaの型に変換して取得するためのAPIです。 - -取得するカラムの数に応じて`*:`演算子を使用して変換する型を指定します。 - -例では、usersテーブルのid, name, ageカラムを取得する場合を示しておりそれぞれのカラムの型を指定しています。 - -```scala 3 -result.decode(bigint *: varchar *: int.opt) -``` - -NULL許容のカラムを取得する場合は`Option`型に変換するために`opt`メソッドを使用します。 -これによりレコードがNULLの場合はNoneとして取得することができます。 - -クエリ実行からレコード取得までの一連の流れは以下のようになります。 - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - decodes <- result.decode(bigint *: varchar *: int.opt) - yield decodes -} -``` - -`ResultSet`から取得するレコードは常に配列になります。 -これはMySQLで実行するクエリの結果が常に複数のレコードを返す可能性があるからです。 - -単一のレコードを取得する場合は`decode`処理後に、`head`や`headOption`メソッドを使用して取得を行なってください。 - -現在のバージョンでは以下のデータ型がサポートされています。 - -| Codec | データ型 | Scala 型 | -|---------------|---------------------|------------------| -| `boolean` | `BOOLEAN` | `Boolean` | -| `tinyint` | `TINYINT` | `Byte` | -| `utinyint` | `unsigned TINYINT` | `Short` | -| `smallint` | `SMALLINT` | `Short` | -| `usmallint` | `unsigned SMALLINT` | `Int` | -| `int` | `INT` | `Int` | -| `uint` | `unsigned INT` | `Long` | -| `bigint` | `BIGINT` | `Long` | -| `ubigint` | `unsigned BIGINT` | `BigInt` | -| `float` | `FLOAT` | `Float` | -| `double` | `DOUBLE` | `Double` | -| `decimal` | `DECIMAL` | `BigDecimal` | -| `char` | `CHAR` | `String` | -| `varchar` | `VARCHAR` | `String` | -| `binary` | `BINARY` | `Array[Byte]` | -| `varbinary` | `VARBINARY` | `String` | -| `tinyblob` | `TINYBLOB` | `String` | -| `blob` | `BLOB` | `String` | -| `mediumblob` | `MEDIUMBLOB` | `String` | -| `longblob` | `LONGBLOB` | `String` | -| `tinytext` | `TINYTEXT` | `String` | -| `text` | `TEXT` | `String` | -| `mediumtext` | `MEDIUMTEXT` | `String` | -| `longtext` | `LONGTEXT` | `String` | -| `enum` | `ENUM` | `String` | -| `set` | `SET` | `List[String]` | -| `json` | `JSON` | `String` | -| `date` | `DATE` | `LocalDate` | -| `time` | `TIME` | `LocalTime` | -| `timetz` | `TIME` | `OffsetTime` | -| `datetime` | `DATETIME` | `LocalDateTime` | -| `timestamp` | `TIMESTAMP` | `LocalDateTime` | -| `timestamptz` | `TIMESTAMP` | `OffsetDateTime` | -| `year` | `YEAR` | `Year` | - -※ 現在MySQLのデータ型を指定して値を取得するような作りとなっていますが、将来的にはより簡潔にScalaの型を指定して値を取得するような作りに変更する可能性があります。 - -以下サポートされていないデータ型があります。 - -- GEOMETRY -- POINT -- LINESTRING -- POLYGON -- MULTIPOINT -- MULTILINESTRING -- MULTIPOLYGON -- GEOMETRYCOLLECTION - -## トランザクション - -`Connection`を使用してトランザクションを実行するためには`setAutoCommit`メソッドと`commit`メソッド、`rollback`メソッドを組み合わせて使用します。 - -まず、`setAutoCommit`メソッドを使用してトランザクションの自動コミットを無効にします。 - -```scala 3 -conn.setAutoCommit(false) -``` - -何かしらの処理を行った後に`commit`メソッドを使用してトランザクションをコミットします。 - -```scala 3 -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.commit() -yield -``` -もしくは、`rollback`メソッドを使用してトランザクションをロールバックします。 - -```scala 3 -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.rollback() -yield -``` - -`setAutoCommit`メソッドを使用してトランザクションの自動コミットを無効にした場合、コネクションのResourceを解放する際に自動的にロールバックが行われます。 - -### トランザクション分離レベル - -ldbcではトランザクション分離レベルの設定を行うことができます。 - -トランザクション分離レベルは`setTransactionIsolation`メソッドを使用して設定を行います。 - -MySQLでは以下のトランザクション分離レベルがサポートされています。 - -- READ UNCOMMITTED -- READ COMMITTED -- REPEATABLE READ -- SERIALIZABLE - -MySQLのトランザクション分離レベルについては[公式ドキュメント](https://dev.mysql.com/doc/refman/8.0/ja/innodb-transaction-isolation-levels.html)を参照してください。 - -```scala 3 -import ldbc.connector.Connection.TransactionIsolationLevel - -conn.setTransactionIsolation(TransactionIsolationLevel.REPEATABLE_READ) -``` - -現在設定されているトランザクション分離レベルを取得するには`getTransactionIsolation`メソッドを使用します。 - -```scala 3 -for - isolationLevel <- conn.getTransactionIsolation() -yield -``` - -### セーブポイント - -より高度なトランザクション管理のために「Savepoint機能」を使用することができます。これにより、データベース操作中に特定のポイントをマークしておくことが可能になり、何か問題が発生した場合にも、そのポイントまでデータベースの状態を巻き戻すことができます。これは、複雑なデータベース操作や、長いトランザクションの中での安全なポイント設定を必要とする場合に特に役立ちます。 - -**特徴:** - -- 柔軟なトランザクション管理:Savepointを使って、トランザクション内の任意の場所で「チェックポイント」を作成。必要に応じてそのポイントまで状態を戻すことができます。 -- エラー回復:エラーが発生した時、全てを最初からやり直すのではなく、最後の安全なSavepointまで戻ることで、時間の節約と効率の向上が見込めます。 -- 高度な制御:複数のSavepointを設定することで、より精密なトランザクション制御が可能に。開発者はより複雑なロジックやエラーハンドリングを簡単に実装できます。 - -この機能を活用することで、あなたのアプリケーションはより堅牢で信頼性の高いデータベース操作を実現できるようになります。 - -**セーブポイントの設定** - -Savepointを設定するには、`setSavepoint`メソッドを使用します。このメソッドは、Savepointの名前を指定することができます。 -Savepointの名前を指定しない場合、デフォルトの名前としてUUIDで生成された値が設定されます。 - -`getSavepointName`メソッドを使用して、設定されたSavepointの名前を取得することができます。 - -※ MySQLではデフォルトで自動コミットが有効になっているため、Savepointを使用する場合は自動コミットを無効にする必要があります。そうしないと全ての処理が都度コミットされてしまうため、Savepointを使用したトランザクションのロールバックを行うことができなくなるためです。 - -```scala 3 -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") -yield savepoint.getSavepointName -``` - -**セーブポイントのロールバック** - -Savepointを使用してトランザクションの一部をロールバックするには、`rollback`メソッドにSavepointを渡すことでロールバックを行います。 -Savepointを使用して部分的にロールバックをした後、トランザクション全体をコミットするとそのSavepoint以降のトランザクションはコミットされません。 - -```scala 3 -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.rollback(savepoint) - _ <- conn.commit() -yield -``` - -**セーブポイントのリリース** - -Savepointをリリースするには、`releaseSavepoint`メソッドにSavepointを渡すことでリリースを行います。 -Savepointをリリースした後、トランザクション全体をコミットするとそのSavepoint以降のトランザクションはコミットされます。 - -```scala 3 -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.releaseSavepoint(savepoint) - _ <- conn.commit() -yield -``` - -## ユーティリティコマンド - -MySQLにはいくつかのユーティリティコマンドがあります。([参照](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase_utility.html)) - -ldbcではこれらのコマンドを使用するためのAPIを提供しています。 - -| コマンド | 用途 | サポート | -|------------------------|--------------------------------------|------| -| `COM_QUIT` | `クライアントが接続を閉じることをサーバーに要求していることを伝える。` | ✅ | -| `COM_INIT_DB` | `接続のデフォルト・スキーマを変更する` | ✅ | -| `COM_STATISTICS` | `内部ステータスの文字列を可読形式で取得する。` | ✅ | -| `COM_DEBUG` | `サーバーの標準出力にデバッグ情報をダンプする` | ❌ | -| `COM_PING` | `サーバーが生きているかチェックする` | ✅ | -| `COM_CHANGE_USER` | `現在の接続のユーザーを変更する` | ✅ | -| `COM_RESET_CONNECTION` | `セッションの状態をリセットする` | ✅ | -| `COM_SET_OPTION` | `現在の接続のオプションを設定する` | ✅ | - -### COM QUIT - -`COM_QUIT`はクライアントが接続を閉じることをサーバーに要求していることを伝えるためのコマンドです。 - -ldbcでは`Connection`の`close`メソッドを使用して接続を閉じることができます。 -`close`メソッドを使用すると接続が閉じられるため、その後の処理で接続を使用することはできません。 - -※ `Connection`は`Resource`を使用してリソース管理を行います。そのため`close`メソッドを使用してリソースの解放を行う必要はありません。 - -```scala 3 -connection.use { conn => - conn.close() -} -``` - -### COM INIT DB - -`COM_INIT_DB`は接続のデフォルト・スキーマを変更するためのコマンドです。 - -ldbcでは`Connection`の`setSchema`メソッドを使用してデフォルト・スキーマを変更することができます。 - -```scala 3 -connection.use { conn => - conn.setSchema("test") -} -``` - -### COM STATISTICS - -`COM_STATISTICS`は内部ステータスの文字列を可読形式で取得するためのコマンドです。 - -ldbcでは`Connection`の`getStatistics`メソッドを使用して内部ステータスの文字列を取得することができます。 - -```scala 3 -connection.use { conn => - conn.getStatistics -} -``` - -取得できるステータスは以下のようになります。 - -- `uptime` : サーバーが起動してからの時間 -- `threads` : 現在接続しているクライアントの数 -- `questions` : サーバーが起動してからのクエリの数 -- `slowQueries` : 遅いクエリの数 -- `opens` : サーバーが起動してからのテーブルのオープン数 -- `flushTables` : サーバーが起動してからのテーブルのフラッシュ数 -- `openTables` : 現在オープンしているテーブルの数 -- `queriesPerSecondAvg` : 秒間のクエリの平均数 - -### COM PING - -`COM_PING`はサーバーが生きているかチェックするためのコマンドです。 - -ldbcでは`Connection`の`isValid`メソッドを使用してサーバーが生きているかチェックすることができます。 -サーバーが生きている場合は`true`を返却し、生きていない場合は`false`を返却します。 - -```scala 3 -connection.use { conn => - conn.isValid -} -``` - -### COM CHANGE USER - -`COM_CHANGE_USER`は現在の接続のユーザーを変更するためのコマンドです。 -また、以下の接続状態をリセットします。 - -- ユーザー変数 -- 一時テーブル -- プリペアド・ステートメント -- etc... - -ldbcでは`Connection`の`changeUser`メソッドを使用してユーザーを変更することができます。 - -```scala 3 -connection.use { conn => - conn.changeUser("root", "password") -} -``` - -### COM RESET CONNECTION - -`COM_RESET_CONNECTION`はセッションの状態をリセットするためのコマンドです。 - -`COM_RESET_CONNECTION`は`COM_CHANGE_USER`をより軽量化したもので、セッションの状態をクリーンアップする機能はほぼ同じだが、次のような機能がある - -- 再認証を行わない(そのために余分なクライアント/サーバ交換を行わない)。 -- 接続を閉じない。 - -ldbcでは`Connection`の`resetServerState`メソッドを使用してセッションの状態をリセットすることができます。 - -```scala 3 -connection.use { conn => - conn.resetServerState -} -``` - -### COM SET OPTION - -`COM_SET_OPTION`は現在の接続のオプションを設定するためのコマンドです。 - -ldbcでは`Connection`の`enableMultiQueries`メソッドと`disableMultiQueries`メソッドを使用してオプションを設定することができます。 - -`enableMultiQueries`メソッドを使用すると、複数のクエリを一度に実行することができます。 -`disableMultiQueries`メソッドを使用すると、複数のクエリを一度に実行することができなくなります。 - -※ これは、Insert、Update、および Delete ステートメントによるバッチ処理にのみ使用できます。Selectステートメントで使用を行なったとしても、最初のクエリの結果のみが返されます。 - -```scala 3 -connection.use { conn => - conn.enableMultiQueries *> conn.disableMultiQueries -} -``` - -## バッチコマンド - -ldbcではバッチコマンドを使用して複数のクエリを一度に実行することができます。 -バッチコマンドを使用することで、複数のクエリを一度に実行することができるため、ネットワークラウンドトリップの回数を減らすことができます。 - -バッチコマンドを使用するには`Statement`または`PreparedStatement`の`addBatch`メソッドを使用してクエリを追加し、`executeBatch`メソッドを使用してクエリを実行します。 - -```scala 3 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} -``` - -上記の例では、`Alice`と`Bob`のデータを一度に追加することができます。 -実行されるクエリは以下のようになります。 - -```sql -INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -バッチコマンド実行後の戻り値は、実行したクエリそれぞれの影響を受けた行数の配列となります。 - -上記の例では、`Alice`のデータは1行追加され、`Bob`のデータも1行追加されるため、戻り値は`List(1, 1)`となります。 - -バッチコマンドを実行した後は、今まで`addBatch`メソッドで追加したクエリがクリアされます。 - -手動でクリアする場合は`clearBatch`メソッドを使用してクリアを行います。 - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.clearBatch() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - _ <- statement.executeBatch() - yield -} -``` - -上記の例では、`Alice`のデータは追加されませんが、`Bob`のデータは追加されます。 - -### StatementとPreparedStatementの違い - -`Statement`と`PreparedStatement`ではバッチコマンドで実行されるクエリが異なる場合があります。 - -`Statement`を使用してINSERT文をバッチコマンドで実行した場合、複数のクエリが一度に実行されます。 -しかし、`PreparedStatement`を使用してINSERT文をバッチコマンドで実行した場合、1つのクエリが実行されます。 - -例えば、以下のクエリをバッチコマンドで実行した場合、`Statement`を使用しているため、複数のクエリが一度に実行されます。 - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} - -// 実行されるクエリ -// INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -しかし、以下のクエリをバッチコマンドで実行した場合、`PreparedStatement`を使用しているため、1つのクエリが実行されます。 - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.addBatch() - _ <- statement.setString(1, "Bob") - _ <- statement.setInt(2, 30) - _ <- statement.addBatch() - result <- statement.executeBatch() - yield result -} - -// 実行されるクエリ -// INSERT INTO users (name, age) VALUES ('Alice', 20), ('Bob', 30); -``` - -これは、`PreparedStatement`を使用している場合、クエリのパラメーターを設定した後に`addBatch`メソッドを使用することで、1つのクエリに複数のパラメーターを設定することができるためです。 - -## ストアドプロシージャの実行 - -ldbcではストアドプロシージャを実行するためのAPIを提供しています。 - -ストアドプロシージャを実行するには`Connection`の`prepareCall`メソッドを使用して`CallableStatement`を構築します。 - -※ 使用するストアドプロシージャは[公式](https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-statements-callable.html)ドキュメント記載のものを使用しています。 - -```sql -CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) -BEGIN - DECLARE z INT; - SET z = inOutParam + 1; - SET inOutParam = z; - - SELECT inputParam; - - SELECT CONCAT('zyxw', inputParam); -END -``` - -上記のストアドプロシージャを実行する場合は以下のようになります。 - -```scala 3 -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - hasResult <- callableStatement.execute() - values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { - for - resultSet <- callableStatement.getResultSet().flatMap { - case Some(rs) => IO.pure(rs) - case None => IO.raiseError(new Exception("No result set")) - } - value <- resultSet.getString(1) - yield value - } - yield values // List(Some("abcdefg"), Some("zyxwabcdefg")) -} -``` - -出力パラメータ(ストアド・プロシージャを作成したときにOUTまたはINOUTとして指定したパラメータ)の値を取得するには、JDBCでは、CallableStatementインターフェイスのさまざまな`registerOutputParameter()`メソッドを使用して、ステートメント実行前にパラメータを指定する必要がありますが、ldbcでは`setXXX`メソッドを使用してパラメータを設定することだけクエリ実行時にパラメーターの設定も行なってくれます。 - -ただし、ldbcでも`registerOutputParameter()`メソッドを使用してパラメータを指定することもできます。 - -```scala 3 -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) - hasResult <- callableStatement.execute() - value <- callableStatement.getInt(2) - yield value // 2 -} -``` - -※ `registerOutParameter`でOutパラメータを指定する場合、同じindex値を使用して`setXXX`メソッドでパラメータを設定していない場合サーバーには`Null`で値が設定されることに注意してください。 - -## 未対応機能 - -ldbcコネクタは現在実験的な機能となります。そのため、以下の機能はサポートされていません。 -機能提供は順次行っていく予定です。 - -- コネクションプーリング -- フェイルオーバー対策 -- etc... diff --git a/docs/src/main/mdoc/ja/reference/directory.conf b/docs/src/main/mdoc/ja/reference/directory.conf deleted file mode 100644 index 104460f08..000000000 --- a/docs/src/main/mdoc/ja/reference/directory.conf +++ /dev/null @@ -1,5 +0,0 @@ -laika.title = Reference -laika.navigationOrder = [ - index.md, - Connector.md -] diff --git a/docs/src/main/mdoc/ja/reference/index.md b/docs/src/main/mdoc/ja/reference/index.md deleted file mode 100644 index cb73b5af7..000000000 --- a/docs/src/main/mdoc/ja/reference/index.md +++ /dev/null @@ -1,14 +0,0 @@ -{% - laika.title = はじめに - laika.metadata.language = ja -%} - -# Reference - -このセクションでは、ldbcのコア抽象化と機械の詳細について説明します。 - -## 目次 - -@:navigationTree { - entries = [ { target = "/ja/reference", depth = 2 } ] -} diff --git a/docs/src/main/mdoc/ja/tutorial/Connection.md b/docs/src/main/mdoc/ja/tutorial/Connection.md deleted file mode 100644 index 658ebd343..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Connection.md +++ /dev/null @@ -1,93 +0,0 @@ -{% - laika.title = コネクション - laika.metadata.language = ja -%} - -# コネクション - -この章では、データベースに接続するためのコネクション構築方法について説明します。 - -データベースに接続するためには、コネクションを構築する必要がある。コネクションは、データベースへの接続を管理するためのリソースである。コネクションは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 - -ldbcはjdbcとldbc独自のコネクタのどちらかを使ってデータベースに接続する。どちらを使うかは設定する依存関係によって決まる。 - -## Use jdbc connector - -まず、依存関係を追加します。 - -jdbcコネクタを使用する場合、MySQLのコネクタも追加する必要があります。 - -```scala -//> dep "@ORGANIZATION@::jdbc-connector:@VERSION@" -//> dep "com.mysql":"mysql-connector-j":"@MYSQL_VERSION@" -``` - -次に、`MysqlDataSource`を使用してデータソースを作成します。 - -```scala -val ds = new com.mysql.cj.jdbc.MysqlDataSource() -ds.setServerName("127.0.0.1") -ds.setPortNumber(13306) -ds.setDatabaseName("world") -ds.setUser("ldbc") -ds.setPassword("password") -``` - -作成したデータソースを使用してjdbcコネクタのデータソースを作成します。 - -```scala -val datasource = jdbc.connector.MysqlDataSource[IO](ds) -``` - -最後に、jdbcコネクタを使用してコネクションを作成します。 - -```scala -val connection: Resource[IO, Connection[IO]] = - Resource.make(datasource.getConnection)(_.close()) -``` - -ここではCats Effectの`Resource`を使用してコネクション使用後にクローズするようにしています。 - -## Use ldbc connector - -まず、依存関係を追加します。 - -```scala -//> dep "@ORGANIZATION@::ldbc-connector:@VERSION@" -``` - -次に、Tracerを提供します。ldbcコネクタはTracerを使用してテレメトリデータの収集を行います。 これらは、アプリケーショントレースを記録するために使用されます。 - -ここでは、`Tracer.noop`を使用してTracerを提供します。 - -```scala -given Tracer[IO] = Tracer.noop[IO] -``` - -最後に、`Connection`を作成します。 - -```scala -val connection: Resource[IO, Connection[IO]] = - ldbc.connector.Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "ldbc", - password = Some("password"), - database = Some("ldbc") - ) -``` - -コネクションを設定するためのパラメータは以下の通りです。 - -| プロパティ | 詳細 | 必須 | -|---------------------------|----------------------------------------------------------------|----| -| `host` | `データベースホスト情報` | ✅ | -| `port` | `データベースポート情報` | ✅ | -| `user` | `データベースユーザー情報` | ✅ | -| `password` | `データベースパスワード情報 (default: None)` | ❌ | -| `database` | `データベース名情報 (default: None)` | ❌ | -| `debug` | `デバッグ情報を表示するかどうか (default: false)` | ✅ | -| `ssl` | `SSLの設定 (default: SSL.None)` | ✅ | -| `socketOptions` | `TCP/ UDP ソケットのソケットオプションを指定する (default: defaultSocketOptions)` | ✅ | -| `readTimeout` | `タイムアウト時間を指定する (default: Duration.Inf)` | ✅ | -| `allowPublicKeyRetrieval` | `公開鍵を取得するかどうか (default: false)` | ✅ | diff --git a/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md b/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md deleted file mode 100644 index 7bd18bb23..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md +++ /dev/null @@ -1,68 +0,0 @@ -{% - laika.title = カスタム データ型 - laika.metadata.language = ja -%} - -# カスタム データ型 - -この章では、ldbcで構築したテーブル定義でユーザー独自の型もしくはサポートされていない型を使用するための方法について説明します。 - -セットアップで作成したテーブル定義に新たにカラムを追加します。 - -```sql -ALTER TABLE user ADD COLUMN status BOOLEAN NOT NULL DEFAULT TRUE; -``` - -## Encoder - -ldbcではstatementに受け渡す値を`Encoder`で表現しています。`Encoder`はstatementへのバインドする値を表現するためのtraitです。 - -`Encoder`を実装することでstatementに受け渡す値をカスタム型で表現することができます。 - -ユーザー情報にそのユーザーのステータスを表す`Status`を追加します。 - -```scala 3 -enum Status(val done: Boolean, val name: String): - case Active extends Status(false, "Active") - case InActive extends Status(true, "InActive") -``` - -以下のコード例では、カスタム型の`Encoder`を定義しています。 - -これによりstatementにカスタム型をバインドすることができるようになります。 - -```scala 3 -given Encoder[Status] with - override def encode(status: Status): Boolean = status.done -``` - -カスタム型は他のパラメーターと同じようにstatementにバインドすることができます。 - -```scala -val program1: Executor[IO, Int] = - sql"INSERT INTO user (name, email, status) VALUES (${ "user 1" }, ${ "user@example.com" }, ${ Status.Active })".update -``` - -これでstatementにカスタム型をバインドすることができるようになりました。 - -## Decoder - -ldbcではパラメーターの他に実行結果から独自の型を取得するための`Decoder`も提供しています。 - -`Decoder`を実装することでstatementの実行結果から独自の型を取得することができます。 - -以下のコード例では、`Decoder.Elem`を使用して単一のデータ型を取得する方法を示しています。 - -```scala 3 -given Decoder.Elem[Status] = Decoder.Elem.mapping[Boolean, Status] { - case true => Status.Active - case false => Status.InActive -} -``` - -```scala 3 -val program2: Executor[IO, (String, String, Status)] = - sql"SELECT name, email, status FROM user WHERE id = 1".query[(String, String, Status)].unsafe -``` - -これでstatementの実行結果からカスタム型を取得することができるようになりました。 diff --git a/docs/src/main/mdoc/ja/tutorial/Database-Operations.md b/docs/src/main/mdoc/ja/tutorial/Database-Operations.md deleted file mode 100644 index 17c04c0f0..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Database-Operations.md +++ /dev/null @@ -1,53 +0,0 @@ -{% - laika.title = データベース操作 - laika.metadata.language = ja -%} - -# データベース操作 - -このセクションでは、データベース操作について説明します。 - -データベース接続を行う前にコミットのタイミングや読み書き専用などの設定を行う必要があります。 - -## 読み取り専用 - -読み取り専用のトランザクションを開始するには、`readOnly`メソッドを使用します。 - -`readOnly`メソッドを使用することで実行するクエリの処理を読み込み専用にすることができます。`readOnly`メソッドは`insert/update/delete`文でも使用することができますが、書き込み処理を行うので実行時にエラーとなります。 - -```scala -val read = sql"SELECT 1".query[Int].to[Option].readOnly(connection) -``` - -## 書き込み - -書き込みを行うには、`commit`メソッドを使用します。 - -`commit`メソッドを使用することで実行するクエリの処理をクエリ実行時ごとにコミットするように設定することができます。 - -```scala -val write = sql"INSERT INTO `table`(`c1`, `c2`) VALUES ('column 1', 'column 2')".update.commit(connection) -``` - -## トランザクション - -トランザクションを開始するには、`transaction`メソッドを使用します。 - -`transaction`メソッドを使用することで複数のデータベース接続処理を1つのトランザクションにまとめることができます。 - -ldbcは`Executor[F, A]`という形式でデータベースへの接続処理を組むことになる。 Executorはモナドなので、for内包を使って2つの小さなプログラムを1つの大きなプログラムにすることができる。 - -```scala -val program: Executor[IO, (List[Int], Option[Int], Int)] = - for - result1 <- sql"SELECT 1".query[Int].to[List] - result2 <- sql"SELECT 2".query[Int].to[Option] - result3 <- sql"SELECT 3".query[Int].unsafe - yield (result1, result2, result3) -``` - -1つのプログラムとなった`Executor`を`transaction`メソッドで1つのトランザクションでまとめて処理を行うことができます。 - -```scala -val transaction = program.transaction(connection) -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Error-Handling.md b/docs/src/main/mdoc/ja/tutorial/Error-Handling.md deleted file mode 100644 index 1c2ea1b42..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Error-Handling.md +++ /dev/null @@ -1,37 +0,0 @@ -{% - laika.title = エラーハンドリング - laika.metadata.language = ja -%} - -# エラーハンドリング - -この章では、例外をトラップしたり処理したりするプログラムを構築するためのコンビネーター一式を検討する。 - -## 例外について - -ある操作が成功するかどうかは、ネットワークの健全性、テーブルの現在の内容、ロックの状態など、予測できない要因に依存します。そのため、`EitherT[Executor, Throwable, A]`のような論理和ですべてを計算するか、明示的に捕捉されるまで例外の伝播を許可するかを決めなければならない。つまり、ldbcのアクション(ターゲット・モナドに変換される)が実行されると、例外が発生する可能性がある。 - -発生しやすい例外は主に3種類ある - -1. あらゆる種類のI/Oで様々なタイプのIOExceptionが発生する可能性があり、これらの例外は回復できない傾向がある。 -2. データベース例外は、通常、ベンダー固有のSQLStateで特定のエラーを識別する一般的なSQLExceptionとして、キー違反のような一般的な状況で発生します。エラーコードは伝承として伝えられるか、実験によって発見されなければなりません。XOPENとSQL:2003の標準がありますが、どのベンダーもこれらの仕様に忠実ではないようです。これらのエラーには回復可能なものとそうでないものがある。 -3. ldbcは、無効な型マッピング、ドライバから返される未知の JDBC 定数、観測される NULL 値、その他 ldbc が想定している不変条件の違反に対して InvariantViolation を発生させます。これらの例外はプログラマのエラーかドライバの不適合を示し、一般に回復不可能です。 - -## モナド・エラーと派生コンバイネーター - -すべてのldbcモナドは、`MonadError[?[_], Throwable]`を拡張したAsyncインスタンスを提供する。つまり、Executorなどは以下のようなプリミティブな操作を持つことになる - -- raiseError: 例外を発生させる (Throwableを`M[A]`に変換する) -- handleErrorWith: 例外を処理する (`M[A]`を`M[B]`に変換する) -- attempt: 例外を捕捉する (`M[A]`を`M[Either[Throwable, A]]`に変換する) - -つまり、どんなldbcプログラムでも`attempt`を加えるだけで例外を捕捉することができるのだ。 - -```scala -val program = Executor.pure[IO, Int](1) - -program.attempt -// Executor[IO, Either[Throwable, Int]] -``` - -`attempt`と`raiseError`コンビネータから、Catsのドキュメントで説明されているように、他の多くの操作を派生させることができます。 diff --git a/docs/src/main/mdoc/ja/tutorial/Logging.md b/docs/src/main/mdoc/ja/tutorial/Logging.md deleted file mode 100644 index c00a94b33..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Logging.md +++ /dev/null @@ -1,52 +0,0 @@ -{% - laika.title = ロギング - laika.metadata.language = ja -%} - -# ロギング - -ldbcではデータベース接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 - -標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 - -```scala 3 -given LogHandler[IO] = LogHandler.console[IO] -``` - -任意のロギングライブラリを使用してログをカスタマイズする場合は`ldbc.dsl.logging.LogHandler`を使用します。 - -以下は標準実装のログ実装です。ldbcではデータベース接続で以下3種類のイベントが発生します。 - -- Success: 処理の成功 -- ProcessingFailure: データ取得後もしくはデータベース接続前の処理のエラー -- ExecFailure: データベースへの接続処理のエラー - -それぞれのイベントでどのようなログを書き込むかをパターンマッチングによって振り分けを行います。 - -```scala 3 -def console[F[_]: Console: Sync]: LogHandler[F] = - case LogEvent.Success(sql, args) => - Console[F].println( - s"""Successful Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) - case LogEvent.ProcessingFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed ResultSet Processing: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) - case LogEvent.ExecFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md b/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md deleted file mode 100644 index 4a911d4a9..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md +++ /dev/null @@ -1,112 +0,0 @@ -{% - laika.title = パラメータ - laika.metadata.language = ja -%} - -# パラメータ化されたクエリ - -この章では、パラメータ化されたクエリを構築する方法を学びます。 - -## パラメータの追加 - -まずは、パラメーターを持たないクエリを作成します。 - -```scala -sql"SELECT name, email FROM user".query[(String, String)].to[List] -``` - -次にクエリをメソッドに組み込んで、ユーザーが指定する`id`と一致するデータのみを選択するパラメーターを追加してみましょう。文字列の補間を行うのと同じように、`id`引数を`$id`としてSQL文に挿入します。 - -```scala -val id = 1 - -sql"SELECT name, email FROM user WHERE id = $id".query[(String, String)].to[List] -``` - -コネクションを使用してクエリを実行すると問題なく動作します。 - -```scala -connection.use { conn => - sql"SELECT name, email FROM user WHERE id = $id" - .query[(String, String)] - .to[List] - .readOnly(conn) -} -``` - -ここでは何が起こっているのでしょうか?文字列リテラルをSQL文字列にドロップしているだけのように見えますが、実際には`PreparedStatement`を構築しており、`id`値は最終的に`setInt`の呼び出しによって設定されます。 - -## 複数のパラメータ - -複数のパラメータも同じように機能する。驚きはない。 - -```scala -val id = 1 -val email = "alice@example.com" - -connection.use { conn => - sql"SELECT name, email FROM user WHERE id = $id AND email > $email" - .query[(String, String)] - .to[List] - .readOnly(conn) -} -``` - -## IN句の扱い - -SQLリテラルを扱う際によくあるイラつきは、一連の引数をIN句にインライン化したいという欲求ですが、SQLはこの概念をサポートしていません(JDBCも何もサポートしていません)。 - -```scala -val ids = NonEmptyList.of(1, 2, 3) - -connection.use { conn => - (sql"SELECT name, email FROM user WHERE" ++ in("id", ids)) - .query[(String, String)] - .to[List] - .readOnly(conn) -} -``` - -IN句は空であってはならないので、`ids`は`NonEmptyList`であることに注意。 - -このクエリーを実行すると、望ましい結果が得られる - -ldbcでは他にもいくつかの便利な関数が用意されています。 - -- `values` - VALUES句を生成する -- `in` - IN句を生成する -- `notIn` - NOT IN句を生成する -- `and` - AND句を生成する -- `or` - OR句を生成する -- `whereAnd` - AND句で括られた複数の条件のWHERE句を生成する -- `whereOr` - OR句で括られた複数の条件のWHERE句を生成する -- `set` - SET句を生成する -- `orderBy` - ORDER BY句を生成する - -## 静的なパラメーター - -パラメーターは動的ではありますが、時にはパラメーターとして使用しても静的な値として扱いたいことがあるかと思います。 - -例えば受け取った値に応じて取得するカラムを変更する場合、以下のように記述できます。 - -```scala -val column = "name" - -sql"SELECT $column FROM user".query[String].to[List] -``` - -動的なパラメーターは`PreparedStatement`によって処理が行われるため、クエリ文字列自体は`?`で置き換えられます。 - -そのため、このクエリは`SELECT ? FROM user`として実行されます。 - -これではログに出力されるクエリがわかりにくいため、`$column`は静的な値として扱いたい場合は、`$column`を`${sc(column)}`とすることで、クエリ文字列に直接埋め込まれるようになります。 - -```scala -val column = "name" - -sql"SELECT ${sc(column)} FROM user".query[String].to[List] -``` - -このクエリは`SELECT name FROM user`として実行されます。 - -> `sc(...)`は渡された文字列のエスケープを行わないことに注意してください。ユーザから与えられたデータを渡すことは、インジェクションのリスクになります。 diff --git a/docs/src/main/mdoc/ja/tutorial/Query-Builder.md b/docs/src/main/mdoc/ja/tutorial/Query-Builder.md deleted file mode 100644 index 7f054e60a..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Query-Builder.md +++ /dev/null @@ -1,392 +0,0 @@ -{% - laika.title = クエリビルダー - laika.metadata.language = ja -%} - -# クエリビルダー - -この章では、型安全にクエリを構築するための方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -```scala -//> using dep "@ORGANIZATION@::ldbc-query-builder:@VERSION@" -``` - -ldbcでは、クラスを使用してクエリを構築します。 - -```scala 3 -import ldbc.query.builder.* - -case class User(id: Int, name: String, email: String) derives Table -``` - -`User`クラスは`Table`トレイトを継承しています。`Table`トレイトは`Table`クラスを継承しているため、`Table`クラスのメソッドを使用してクエリを構築することができます。 - -```scala -val query = Table[User] - .select(user => (user.id, user.name, user.email)) - .where(_.email === "alice@example.com") -``` - -## SELECT - -型安全にSELECT文を構築する方法はTableが提供する`select`メソッドを使用することです。ldbcではプレーンなクエリに似せて実装されているため直感的にクエリ構築が行えます。またどのようなクエリが構築されているかも一目でわかるような作りになっています。 - -特定のカラムのみ取得を行うSELECT文を構築するには`select`メソッドで取得したいカラムを指定するだけです。 - -```scala -val select = Table[User].select(_.id) - -select.statement === "SELECT id FROM user" -``` - -複数のカラムを指定する場合は`select`メソッドで取得したいカラムを指定して指定したカラムのタプルを返すだけです。 - -```scala -val select = Table[User].select(user => (user.id, user.name)) - -select.statement === "SELECT id, name FROM user" -``` - -全てのカラムを指定したい場合はTableが提供する`selectAll`メソッドを使用することで構築できます。 - -```scala -val select = Table[User].selectAll - -select.statement === "SELECT id, name, email FROM user" -``` - -特定のカラムの数を取得したい場合は、指定したカラムで`count`を使用することで構築できます。  - -```scala -val select = Table[User].select(_.id.count) - -select.statement === "SELECT COUNT(id) FROM user" -``` - -### WHERE - -クエリに型安全にWhere条件を設定する方法は`where`メソッドを使用することです。 - -```scala -val where = Table[User].selectAll.where(_.email === "alice@example.com") - -where.statement === "SELECT id, name, email FROM user WHERE email = ?" -``` - -`where`メソッドで使用できる条件の一覧は以下です。 - -| 条件 | ステートメント | -|----------------------------------------|---------------------------------------| -| `===` | `column = ?` | -| `>=` | `column >= ?` | -| `>` | `column > ?` | -| `<=` | `column <= ?` | -| `<` | `column < ?` | -| `<>` | `column <> ?` | -| `!==` | `column != ?` | -| `IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL")` | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | -| `<=>` | `column <=> ?` | -| `IN (value, value, ...)` | `column IN (?, ?, ...)` | -| `BETWEEN (start, end)` | `column BETWEEN ? AND ?` | -| `LIKE (value)` | `column LIKE ?` | -| `LIKE_ESCAPE (like, escape)` | `column LIKE ? ESCAPE ?` | -| `REGEXP (value)` | `column REGEXP ?` | -| `<<` (value) | `column << ?` | -| `>>` (value) | `column >> ?` | -| `DIV (cond, result)` | `column DIV ? = ?` | -| `MOD (cond, result)` | `column MOD ? = ?` | -| `^ (value)` | `column ^ ?` | -| `~ (value)` | `~column = ?` | - -### GROUP BY/Having - -クエリに型安全にGROUP BY句を設定する方法は`groupBy`メソッドを使用することです。 - -`groupBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準にグループ化することができます。 - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .groupBy(_._2) - -select.statement === "SELECT id, name FROM user GROUP BY name" -``` - -グループ化すると`select`で取得できるデータの数はグループの数だけとなります。そこでグループ化を行った場合には、グループ化に指定したカラムの値や、用意された関数を使ってカラムの値をグループ単位で集計した結果などを取得することができます。 - -`having`を使用すると`groupBy`によってグループ化されて取得したデータに関して、取得する条件を設定することができます。 - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .groupBy(_._2) - .having(_._1 > 1) - -select.statement === "SELECT id, name FROM user GROUP BY name HAVING id > ?" -``` - -### ORDER BY - -クエリに型安全にORDER BY句を設定する方法は`orderBy`メソッドを使用することです。 - -`orderBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準に昇順、降順で並び替えることができます。 - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .orderBy(_.id) - -select.statement === "SELECT id, name FROM user ORDER BY id" -``` - -昇順/降順を指定したい場合は、それぞれカラムに対して `asc`/`desc`を呼び出すだけです。 - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .orderBy(_.id.asc) - -select.statement === "SELECT id, name FROM user ORDER BY id ASC" -``` - -### LIMIT/OFFSET - -クエリに型安全にLIMIT句とOFFSET句を設定する方法は`limit`/`offset`メソッドを使用することです。 - -`limit`を設定すると`select`を実行した時に取得するデータの行数の上限を設定することができ、`offset`を設定すると何番目からのデータを取得するのかを指定することができます。 - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .limit(1) - .offset(1) - -select.statement === "SELECT id, name FROM user LIMIT ? OFFSET ?" -``` - -## JOIN/LEFT JOIN/RIGHT JOIN - -クエリに型安全にJoinを設定する方法は`join`/`leftJoin`/`rightJoin`メソッドを使用することです。 - -Joinでは以下定義をサンプルとして使用します。 - -```scala 3 -case class User(id: Int, name: String, email: String) derives Table -case class Product(id: Int, name: String, price: BigDecimal) derives Table -case class Order( - id: Int, - userId: Int, - productId: Int, - orderDate: LocalDateTime, - quantity: Int -) derives Table - -val userTable = Table[User] -val productTable = Table[Product] -val orderTable = Table[Order] -``` - -まずシンプルなJoinを行いたい場合は、`join`を使用します。 -`join`の第一引数には結合したいテーブルを渡し、第二引数では結合元のテーブルと結合したいテーブルのカラムで比較を行う関数を渡します。これはJoinにおいてのON句に該当します。 - -Join後の`select`は2つのテーブルからカラムを指定することになります。 - -```scala -val join = userTable.join(orderTable)((user, order) => user.id === order.userId) - .select((user, order) => (user.name, order.quantity)) - -join.statement = "SELECT user.`name`, order.`quantity` FROM user JOIN order ON user.id = order.user_id" -``` - -次に左外部結合であるLeft Joinを行いたい場合は、`leftJoin`を使用します。 -`join`が`leftJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 - -```scala 3 -val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) - .select((user, order) => (user.name, order.quantity)) - -join.statement = "SELECT user.`name`, order.`quantity` FROM user LEFT JOIN order ON user.id = order.user_id" -``` - -シンプルなJoinとの違いは`leftJoin`を使用した場合、結合を行うテーブルから取得するレコードはNULLになる可能性があるということです。 - -そのためldbcでは`leftJoin`に渡されたテーブルから取得するカラムのレコードは全てOption型になります。 - -```scala 3 -val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) - .select((user, order) => (user.name, order.quantity)) // (String, Option[Int]) -``` - -次に右外部結合であるRight Joinを行いたい場合は、`rightJoin`を使用します。 -こちらも`join`が`rightJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 - -```scala 3 -val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) - .select((order, user) => (order.quantity, user.name)) - -join.statement = "SELECT order.`quantity`, user.`name` FROM order RIGHT JOIN user ON order.user_id = user.id" -``` - -シンプルなJoinとの違いは`rightJoin`を使用した場合、結合元のテーブルから取得するレコードはNULLになる可能性があるということです。 - -そのためldbcでは`rightJoin`を使用した結合元のテーブルから取得するカラムのレコードは全てOption型になります。 - -```scala 3 -val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) - .select((order, user) => (order.quantity, user.name)) // (Option[Int], String) -``` - -複数のJoinを行いたい場合は、メソッドチェーンで任意のJoinメソッドを呼ぶことで実現することができます。 - -```scala 3 -val join = - (productTable join orderTable)((product, order) => product.id === order.productId) - .rightJoin(userTable)((_, order, user) => order.userId === user.id) - .select((product, order, user) => (product.name, order.quantity, user.name)) // (Option[String], Option[Int], String)] - -join.statement = - """ - |SELECT - | product.`name`, - | order.`quantity`, - | user.`name` - |FROM product - |JOIN order ON product.id = order.product_id - |RIGHT JOIN user ON order.user_id = user.id - |""".stripMargin -``` - -複数のJoinを行っている状態で`rightJoin`での結合を行うと、今までの結合が何であったかにかかわらず直前まで結合していたテーブルから取得するレコードは全てNULL許容なアクセスとなることに注意してください。 - -## INSERT - -型安全にINSERT文を構築する方法はTableが提供する以下のメソッドを使用することです。 - -- insert -- insertInto -- += -- ++= - -**insert** - -`insert`メソッドには挿入するデータのタプルを渡します。タプルはモデルと同じプロパティの数と型である必要があります。また、挿入されるデータの順番はモデルのプロパティおよびテーブルのカラムと同じ順番である必要があります。 - -```scala 3 -val insert = user.insert((1, "name", "email@example.com")) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" -``` - -複数のデータを挿入したい場合は、`insert`メソッドに複数のタプルを渡すことで構築できます。 - -```scala 3 -val insert = user.insert((1, "name 1", "email+1@example.com"), (2, "name 2", "email+2@example.com")) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -**insertInto** - -`insert`メソッドはテーブルが持つ全てのカラムにデータ挿入を行いますが、特定のカラムに対してのみデータを挿入したい場合は`insertInto`メソッドを使用します。 - -これはAutoIncrementやDefault値を持つカラムへのデータ挿入を除外したい場合などに使用できます。 - -```scala 3 -val insert = user.insertInto(user => (user.name, user.email)).values(("name 3", "email+3@example.com")) - -insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?)" -``` - -複数のデータを挿入したい場合は、`values`にタプルの配列を渡すことで構築できます。 - -```scala 3 -val insert = user.insertInto(user => (user.name, user.email)).values(List(("name 4", "email+4@example.com"), ("name 5", "email+5@example.com"))) - -insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?), (?, ?)" -``` - -**+=** - -`+=`メソッドを使用することでモデルを使用してinsert文を構築することができます。モデルを使用する場合は全てのカラムにデータを挿入してしまうことに注意してください。 - -```scala 3 -val insert = user += User(6, "name 6", "email+6@example.com") - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" -``` - -**++=** - -モデルを使用して複数のデータを挿入したい場合は`++=`メソッドを使用します。 - -```scala 3 -val insert = user ++= List(User(7, "name 7", "email+7@example.com"), User(8, "name 8", "email+8@example.com")) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -### ON DUPLICATE KEY UPDATE - -ON DUPLICATE KEY UPDATE 句を指定し行を挿入すると、UNIQUEインデックスまたはPRIMARY KEYで値が重複する場合、古い行のUPDATEが発生します。 - -ldbcでこの処理を実現する方法は、`Insert`に対して`onDuplicateKeyUpdate`を使用することです。 - -```scala -val insert = user.insert((9, "name", "email+9@example.com")).onDuplicateKeyUpdate(v => (v.name, v.email)) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `email` = new_user.`email`" -``` - -## UPDATE - -型安全にUPDATE文を構築する方法はTableが提供する`update`メソッドを使用することです。 - -`update`メソッドの第1引数にはテーブルのカラム名ではなくモデルのプロパティ名を指定し、第2引数に更新したい値を渡します。第2引数に渡す値の型は第1引数で指定したプロパティの型と同じである必要があります。 - -```scala -val update = user.update("name", "update name") - -update.statement === "UPDATE user SET name = ?" -``` - -第1引数に存在しないプロパティ名を指定した場合コンパイルエラーとなります。 - -```scala 3 -val update = user.update("hoge", "update name") // Compile error -``` - -複数のカラムを更新したい場合は`set`メソッドを使用します。 - -```scala 3 -val update = user.update("name", "update name").set("email", "update-email@example.com") - -update.statement === "UPDATE user SET name = ?, email = ?" -``` - -`set`メソッドには条件に応じてクエリを生成させないようにすることもできます。 - -```scala 3 -val update = user.update("name", "update name").set("email", "update-email@example.com", false) - -update.statement === "UPDATE user SET name = ?" -``` - -モデルを使用してupdate文を構築することもできます。モデルを使用する場合は全てのカラムを更新してしまうことに注意してください。 - -```scala 3 -val update = user.update(User(1, "update name", "update-email@example.com")) - -update.statement === "UPDATE user SET id = ?, name = ?, email = ?" -``` - -## DELETE - -型安全にDELETE文を構築する方法はTableが提供する`delete`メソッドを使用することです。 - -```scala -val delete = user.delete - -delete.statement === "DELETE FROM user" -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Schema-Code-Generation.md b/docs/src/main/mdoc/ja/tutorial/Schema-Code-Generation.md deleted file mode 100644 index 1f0b57850..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Schema-Code-Generation.md +++ /dev/null @@ -1,205 +0,0 @@ -{% - laika.title = スキーマコード生成 - laika.metadata.language = ja -%} - -# スキーマコード生成 - -この章では、ldbcのテーブル定義をSQLファイルから自動生成する方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -```scala 3 -addSbtPlugin("@ORGANIZATION@" % "ldbc-plugin" % "@VERSION@") -``` - -## 生成 - -プロジェクトに対してプラグインを有効にします。 - -```sbt -lazy val root = (project in file(".")) - .enablePlugins(Ldbc) -``` - -解析対象のSQLファイルを配列で指定します。 - -```sbt -Compile / parseFiles := List(baseDirectory.value / "test.sql") -``` - -**プラグインを有効にすることで設定できるキーの一覧** - -| キー | 詳細 | -|----------------------|-------------------------------------------| -| `parseFiles` | `解析対象のSQLファイルのリスト` | -| `parseDirectories` | `解析対象のSQLファイルをディレクトリ単位で指定する` | -| `excludeFiles` | `解析から除外するファイル名のリスト` | -| `customYamlFiles` | `Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト` | -| `classNameFormat` | `クラス名の書式を指定する値` | -| `propertyNameFormat` | `Scalaモデルのプロパティ名の形式を指定する値` | -| `ldbcPackage` | `生成されるファイルのパッケージ名を指定する値` | - -解析対象のSQLファイルの先頭には必ずデータベースのCreate文もしくはUse文を定義する必要があります。ldbcはファイルの解析を1ファイルずつ行い、テーブル定義を生成しデータベースモデルにテーブルのリストを格納させます。 -そのためテーブルがどのデータベースに所属しているかを教えてあげる必要があるからです。 - -```sql -CREATE DATABASE `location`; - -USE `location`; - -DROP TABLE IF EXISTS `country`; -CREATE TABLE country ( - `id` BIGINT AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(255) NOT NULL, - `code` INT NOT NULL -); -``` - -解析対象のSQLファイルにはデータベースのCreate/Use文もしくはテーブル定義のCreate/Drop文のみ記載するようにしなければいけません。 - -## 生成コード - -sbtプロジェクトを起動してコンパイルを実行すると、解析対象のSQLファイルを元に生成されたモデルクラスと、テーブル定義がsbtプロジェクトのtarget配下に生成されます。 - -```shell -sbt compile -``` - -上記SQLファイルから生成されるコードは以下のようなものになります。 - -```scala 3 -package ldbc.generated.location - -import ldbc.schema.* - -case class Country( - id: Long, - name: String, - code: Int -) - -object Country: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -Compileでコードを生成した場合、その生成されたファイルはキャッシュされるので、SQLファイルを変更していない場合再度生成されることはありません。SQLファイルを変更した場合もしくは、cleanコマンドを実行してキャッシュを削除した場合はCompileを実行すると再度コードが生成されます。 -キャッシュを利用せず再度コード生成を行いたい場合は、`generateBySchema`コマンドを実行してください。このコマンドはキャッシュを使用せず常にコード生成を行います。 - -```shell -sbt generateBySchema -``` - -## カスタマイズ - -SQLファイルから生成されるコードの型を別のものに変換したい時があるかもしれません。その場合は`customYamlFiles`にカスタマイズを行うymlファイルを渡してあげることで行うことができます。 - -```sbt -Compile / customYamlFiles := List( - baseDirectory.value / "custom.yml" -) -``` - -ymlファイルの形式は以下のようなものである必要があります。 - -```yaml -database: - name: '{データベース名}' - tables: - - name: '{テーブル名}' - columns: # Optional - - name: '{カラム名}' - type: '{変更したいScalaの型}' - class: # Optional - extends: - - '{モデルクラスに継承させたいtraitなどのpackageパス}' // package.trait.name - object: # Optional - extends: - - '{オブジェクトに継承させたいtraitなどのpackageパス}' - - name: '{テーブル名}' - ... -``` - -`database`は解析対象のSQLファイルに記載されているデータベース名である必要があります。またテーブル名は解析対象のSQLファイルに記載されているデータベースに所属しているテーブル名である必要があります。 - -`columns`には型を変更したいカラム名と変更したいScalaの型を文字列で記載を行います。`columns`には複数の値を設定できますが、nameに記載されたカラム名が対象のテーブルに含まれいてなければなりません。 -また、変換を行うScalaの型はカラムのData型がサポートしている型である必要があります。もしサポート対象外の型を指定したい場合は、`object`に対して暗黙の型変換を行う設定を持ったtraitやabstract classなどを渡してあげる必要があります。 - -Data型がサポートしている型に関しては[こちら](/ja/tutorial/Schema.md#データ型)を、サポート対象外の型を設定する方法は[こちら](/ja/tutorial/Schema.md#カスタム-データ型)を参照してください。 - -Int型をユーザー独自の型であるCountryCodeに変換する場合は、以下のような`CustomMapping`traitを実装します。 - -```scala 3 -trait CountryCode: - val code: Int -object Japan extends CountryCode: - override val code: Int = 1 - -trait CustomMapping: // 任意の名前 - given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode] -``` - -カスタマイズを行うためのymlファイルに実装を行なった`CustomMapping`traitを設定し、対象のカラムの型をCountryCodeに変換してあげます。 - -```yaml -database: - name: 'location' - tables: - - name: 'country' - columns: - - name: 'code' - type: 'Country.CountryCode' // CustomMappingをCountryオブジェクトにミックスインさせるのでそこから取得できるように記載 - object: - extends: - - '{package.name.}CustomMapping' -``` - -上記設定で生成されるコードは以下のようになり、ユーザー独自の型でモデルとテーブル定義を生成できるようになります。 - -```scala 3 -case class Country( - id: Long, - name: String, - code: Country.CountryCode -) - -object Country extends /*{package.name.}*/CustomMapping: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -データベースモデルに関してもSQLファイルから自動生成が行われています。 - -```scala 3 -package ldbc.generated.location - -import ldbc.schema.* - -case class LocationDatabase( - schemaMeta: Option[String] = None, - catalog: Option[String] = Some("def"), - host: String = "127.0.0.1", - port: Int = 3306 -) extends Database: - - override val databaseType: Database.Type = Database.Type.MySQL - - override val name: String = "location" - - override val schema: String = "location" - - override val character: Option[Character] = None - - override val collate: Option[Collate] = None - - override val tables = Set( - Country.table - ) -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Schema.md b/docs/src/main/mdoc/ja/tutorial/Schema.md deleted file mode 100644 index aa1beea69..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Schema.md +++ /dev/null @@ -1,485 +0,0 @@ -{% - laika.title = スキーマ - laika.metadata.language = ja -%} - -# スキーマ - -この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、Code Generatorを使ってこの作業を省略することもできます。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -```scala -//> using dep "@ORGANIZATION@::ldbc-schema:@VERSION@" -``` - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import ldbc.schema.* -import ldbc.schema.attribute.* -``` - -ldbcは、Scalaモデルとデータベースのテーブル定義を1対1のマッピングで管理します。モデルが保持するプロパティとテーブルが保持するカラムのマッピングは、定義順に行われます。テーブル定義は、Create文の構造と非常によく似ています。このため、テーブル定義の構築はユーザーにとって直感的なものとなります。 - -ldbc は、このテーブル定義をさまざまな目的で使用します。型安全なクエリの生成、ドキュメントの生成など。 - -```scala 3 -case class User( - id: Int, - name: String, - email: String, -) - -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), // `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(50)), // `name` VARCHAR(50) NOT NULL, - column("email", VARCHAR(100)), // `email` VARCHAR(100) NOT NULL, -) // ); -``` - -すべてのカラムはcolumnメソッドで定義されます。各カラムにはカラム名、データ型、属性があります。以下のプリミティブ型が標準でサポートされており、すぐに使用できます。 - -- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` -- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` -- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` -- String -- Boolean -- java.time.* - -Null可能な列は`Option[T]`で表現され、Tはサポートされるプリミティブ型の1つです。Option型でない列はすべてNot Nullであることに注意してください。 - -## データ型 - -モデルが持つプロパティのScala型とカラムが持つデータ型の対応付けは、定義されたデータ型がScala型をサポートしている必要があります。サポートされていない型を割り当てようとするとコンパイルエラーが発生します。 - -データ型がサポートするScalaの型は以下の表の通りです。 - -| Data Type | Scala Type | -|--------------|-------------------------------------------------------------------------------------------------| -| `BIT` | `Byte, Short, Int, Long` | -| `TINYINT` | `Byte, Short` | -| `SMALLINT` | `Short, Int` | -| `MEDIUMINT` | `Int` | -| `INT` | `Int, Long` | -| `BIGINT` | `Long, BigInt` | -| `DECIMAL` | `BigDecimal` | -| `FLOAT` | `Float` | -| `DOUBLE` | `Double` | -| `CHAR` | `String` | -| `VARCHAR` | `String` | -| `BINARY` | `Array[Byte]` | -| `VARBINARY` | `Array[Byte]` | -| `TINYBLOB` | `Array[Byte]` | -| `BLOB` | `Array[Byte]` | -| `MEDIUMBLOB` | `Array[Byte]` | -| `LONGBLOB` | `Array[Byte]` | -| `TINYTEXT` | `String` | -| `TEXT` | `String` | -| `MEDIUMTEXT` | `String` | -| `DATE` | `java.time.LocalDate` | -| `DATETIME` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime` | -| `TIMESTAMP` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime` | -| `TIME` | `java.time.LocalTime` | -| `YEAR` | `java.time.Instant, java.time.LocalDate, java.time.Year` | -| `BOOLEA` | `Boolean` | - -**整数型を扱う際の注意点** - -符号あり、符号なしに応じて、扱えるデータの範囲がScalaの型に収まらないことに注意する必要があります。 - -| Data Type | Signed Range | Unsigned Range | Scala Type | Range | -|-------------|----------------------------------------------|----------------------------|------------------|----------------------------------------------------------------------| -| `TINYINT` | `-128 ~ 127` | `0 ~ 255` | `Byte
Short` | `-128 ~ 127
-32768~32767` | -| `SMALLINT` | `-32768 ~ 32767` | `0 ~ 65535` | `Short
Int` | `-32768~32767
-2147483648~2147483647` | -| `MEDIUMINT` | `-8388608 ~ 8388607` | `0 ~ 16777215` | `Int` | `-2147483648~2147483647` | -| `INT` | `-2147483648 ~ 2147483647` | `0 ~ 4294967295` | `Int
Long` | `-2147483648~2147483647
-9223372036854775808~9223372036854775807` | -| `BIGINT` | `-9223372036854775808 ~ 9223372036854775807` | `0 ~ 18446744073709551615` | `Long
BigInt` | `-9223372036854775808~9223372036854775807
...` | - -ユーザー定義の独自型やサポートされていない型を扱う場合は、カスタムデータ型を参照してください。 - -## 属性 - -カラムにはさまざまな属性を割り当てることができます。 - -- `AUTO_INCREMENT` - DDL文を作成し、SchemaSPYを文書化する際に、列を自動インクリメント・キーとしてマークする。 - MySQLでは、データ挿入時にAutoIncでないカラムを返すことはできません。そのため、必要に応じて、ldbcは戻りカラムがAutoIncとして適切にマークされているかどうかを確認します。 -- `PRIMARY_KEY` - DDL文やSchemaSPYドキュメントを作成する際に、列を主キーとしてマークする。 -- `UNIQUE_KEY` - DDL文やSchemaSPYドキュメントを作成する際に、列を一意キーとしてマークする。 -- `COMMENT` - DDL文やSchemaSPY文書を作成する際に、列にコメントを設定する。 - -## キーの設定 - -MySQLではテーブルに対してUniqueキーやIndexキー、外部キーなどの様々なキーを設定することができます。ldbcで構築したテーブル定義でこれらのキーを設定する方法を見ていきましょう。 - -### PRIMARY KEY - -主キー(primary key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムにプライマリーキー制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。また NULL も格納することができません。その結果、プライマリーキー制約が設定されたカラムの値を検索することで、テーブルの中でただ一つのデータを特定することができます。 - -ldbcではこのプライマリーキー制約を2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`PRIMARY_KEY`を渡すだけです。これによって以下の場合 `id`カラムを主キーとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) -``` - -**tableのkeySetメソッドで設定する** - -ldbcのテーブル定義には `keySet`というメソッドが生えており、ここで`PRIMARY_KEY`に主キーとして設定したいカラムを渡すことで主キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => PRIMARY_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`) -// ) -``` - -`PRIMARY_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH -- `Index Option` ldbc.schema.Index.IndexOption - -#### 複合キー (primary key) - -1つのカラムだけではなく、複数のカラムを主キーとして組み合わせ主キーとして設定することもできます。`PRIMARY_KEY`に主キーとして設定したいカラムを複数渡すだけで複合主キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => PRIMARY_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`PRIMARY_KEY`でしか設定することはできません。仮に以下のようにcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを主キーとして設定されてしまいます。 - -ldbcではテーブル定義に複数`PRIMARY_KEY`を設定したとしてもコンパイルエラーにすることはできません。しかし、テーブル定義をクエリの生成やドキュメントの生成などで使用する場合エラーとなります。これはPRIMARY KEYはテーブルごとに1つしか設定することができないという制約によるものです。 - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50), PRIMARY_KEY), - column("email", VARCHAR(100)) -) - -// CREATE TABLE `user` ( -// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, -// ) -``` - -### UNIQUE KEY - -一意キー(unique key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムに一意性制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。 - -ldbcではこの一意性制約を2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`UNIQUE_KEY`を渡すだけです。これによって以下の場合 `id`カラムを一意キーとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) -``` - -**tableのkeySetメソッドで設定する** - -ldbcのテーブル定義には `keySet`というメソッドが生えており、ここで`UNIQUE_KEY`に一意キーとして設定したいカラムを渡すことで一意キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => UNIQUE_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`) -// ) -``` - -`UNIQUE_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String -- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH -- `Index Option` ldbc.schema.Index.IndexOption - -#### 複合キー (unique key) - -1つのカラムだけではなく、複数のカラムを一意キーとして組み合わせ一意キーとして設定することもできます。`UNIQUE_KEY`に一意キーとして設定したいカラムを複数渡すだけで複合一意キーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => UNIQUE_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`UNIQUE_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを一意キーとして設定されてしまいます。 - -### INDEX KEY - -インデックスキー(index key)とはMySQLにおいて目的のレコードを効率よく取得するための「索引」のことです。 - -ldbcではこのインデックスを2つの方法で設定することができます。 - -1. columnメソッドの属性として設定する -2. tableのkeySetメソッドで設定する - -**columnメソッドの属性として設定する** - -columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`INDEX_KEY`を渡すだけです。これによって以下の場合 `id`カラムをインデックスとして設定することができます。 - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) -``` - -**tableのkeySetメソッドで設定する** - -ldbcのテーブル定義には `keySet`というメソッドが生えており、ここで`INDEX_KEY`にインデックスとして設定したいカラムを渡すことでインデックスキーとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => INDEX_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`) -// ) -``` - -`INDEX_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String -- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH -- `Index Option` ldbc.schema.Index.IndexOption - -#### 複合キー (index key) - -1つのカラムだけではなく、複数のカラムをインデックスキーとして組み合わせインデックスキーとして設定することもできます。`INDEX_KEY`にインデックスキーとして設定したいカラムを複数渡すだけで複合インデックスとして設定することができます。 - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => INDEX_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`, `name`) -// ) -``` - -複合キーは`keySet`メソッドでの`INDEX_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合インデックスとしてではなく、それぞれをインデックスキーとして設定されてしまいます。 - -### FOREIGN KEY - -外部キー(foreign key)とは、MySQLにおいてデータの整合性を保つための制約(参照整合性制約)です。 外部キーに設定されているカラムには、参照先となるテーブルのカラム内に存在している値しか設定できません。 - -ldbcではこの外部キー制約をtableのkeySetメソッドを使用する方法で設定することができます。 - -```scala 3 -val user = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - ... -) - .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id))) - -// CREATE TABLE `order` ( -// ..., -// FOREIGN KEY (user_id) REFERENCES `user` (id), -// ) -``` - -`FOREIGN_KEY`メソッドにはカラムとReference値意外にも以下のパラメーターを設定することができます。 - -- `Index Name` String - -外部キー制約には親テーブルの削除時と更新時の挙動を設定することができます。`REFERENCE`メソッドに`onDelete`と`onUpdate`メソッドが提供されているのでこちらを使用することでそれぞれ設定することができます。 - -設定することのできる値は`ldbc.schema.Reference.ReferenceOption`から取得することができます。 - -```scala 3 -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - ... -) - .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id).onDelete(Reference.ReferenceOption.RESTRICT))) - -// CREATE TABLE `order` ( -// ..., -// FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT -// ) -``` - -設定することのできる値は以下になります。 - -- `RESTRICT`: 親テーブルに対する削除または更新操作を拒否します。 -- `CASCADE`: 親テーブルから行を削除または更新し、子テーブル内の一致する行を自動的に削除または更新します。 -- `SET_NULL`: 親テーブルから行を削除または更新し、子テーブルの外部キーカラムを NULL に設定します。 -- `NO_ACTION`: 標準 SQL のキーワード。 MySQLでは、RESTRICT と同等です。 -- `SET_DEFAULT`: このアクションは MySQL パーサーによって認識されますが、InnoDB と NDB はどちらも、ON DELETE SET DEFAULT または ON UPDATE SET DEFAULT 句を含むテーブル定義を拒否します。 - -#### 複合キー (foreign key) - -1つのカラムだけではなく、複数のカラムを外部キーとして組み合わせて設定することもできます。`FOREIGN_KEY`に外部キーとして設定したいカラムを複数渡すだけで複合外部キーとして設定することができます。 - -```scala 3 -val user = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - column("user_email", VARCHAR(100)) - ... -) - .keySet(table => FOREIGN_KEY((table.userId, table.userEmail), REFERENCE(user, (user.id, user.email)))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`user_id`, `user_email`) REFERENCES `user` (`id`, `email`) -// ) -``` - -### 制約名 - -MySQLではCONSTRAINTを使用することで制約に対して任意の名前を付与することができます。この制約名はデータベース単位で一意の値である必要があります。 - -ldbcではCONSTRAINTメソッドが提供されているのでキー制約などの制約を設定する処理をCONSTRAINTメソッドに渡すだけで設定することができます。 - -```scala 3 -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - ... -) - .keySet(table => CONSTRAINT("fk_user_id", FOREIGN_KEY(table.userId, REFERENCE(user, user.id)))) - -// CREATE TABLE `order` ( -// ..., -// CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -// ) -``` - -## カスタム データ型 - -ユーザー独自の型もしくはサポートされていない型を使用するための方法はカラムのデータ型をどのような型として扱うかを教えてあげることです。DataTypeには`mapping`メソッドが提供されているのでこのメソッドを使用して暗黙の型変換として設定します。 - -```scala 3 -case class User( - id: Int, - name: User.Name, - email: String, -) - -object User: - - case class Name(firstName: String, lastName: String) - - given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] - - val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) - ) -``` - -ldbcでは複数のカラムをモデルが持つ1つのプロパティに統合することはできません。ldbcの目的はモデルとテーブルを1対1でマッピングを行い、データベースのテーブル定義を型安全に構築することにあるからです。 - -そのためテーブル定義とモデルで異なった数のプロパティを持つようなことは許可していません。以下のような実装はコンパイルエラーとなります。 - -```scala 3 -case class User( - id: Int, - name: User.Name, - email: String, -) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("first_name", VARCHAR(50)), - column("last_name", VARCHAR(50)), - column("email", VARCHAR(100)) - ) -``` - -上記のような実装を行いたい場合は以下のような実装を検討してください。 - -```scala 3 -case class User( - id: Int, - firstName: String, - lastName: String, - email: String, -): - - val name: User.Name = User.Name(firstName, lastName) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("first_name", VARCHAR(50)), - column("last_name", VARCHAR(50)), - column("email", VARCHAR(100)) - ) -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md deleted file mode 100644 index db3d9eb43..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md +++ /dev/null @@ -1,147 +0,0 @@ -{% - laika.title = データ選択 - laika.metadata.language = ja -%} - -# データ選択 - -この章では、ldbcデータセットを使用してデータを選択する方法を説明します。 - -## コレクションへの行の読み込み - -最初のクエリでは、低レベルのクエリを目指して、いくつかのユーザーをリストに選択し、最初の数件をプリントアウトしてみましょう。ここにはいくつかのステップがあるので、途中のタイプを記しておきます。 - -```scala -sql"SELECT name FROM user" - .query[String] // Query[IO, String] - .to[List] // Executor[IO, List[String]] - .readOnly(conn) // IO[List[String]] - .unsafeRunSync() // List[String] - .foreach(println) // Unit -``` - -これを少し分解してみよう。 - -- `sql"SELECT name FROM user".query[String]`は`Query[IO, String]`を定義し、返される各行をStringにマップする1列のクエリです。このクエリは1列のクエリで、返される行をそれぞれStringにマップします。 -- `.to[List]`は、行をリストに蓄積する便利なメソッドで、この場合は`Executor[IO, List[String]]`を生成します。このメソッドは、CanBuildFromを持つすべてのコレクション・タイプで動作します。 -- `readOnly(conn)`は`IO[List[String]]`を生成し、これを実行すると通常のScala `List[String]`が出力される。 -- `unsafeRunSync()`は、IOモナドを実行し、結果を取得する。これは、IOモナドを実行し、結果を取得するために使用される。 -- `foreach(println)`は、リストの各要素をプリントアウトする。 - -## 複数列クエリ - -もちろん、複数のカラムを選択してタプルにマッピングすることもできます。 - -```scala -sql"SELECT name, email FROM user" - .query[(String, String)] // Query[IO, (String, String)] - .to[List] // Executor[IO, List[(String, String)]] - .readOnly(conn) // IO[List[(String, String)]] - .unsafeRunSync() // List[(String, String)] - .foreach(println) // Unit -``` - -## クラスへのマッピング - -ldbcは、複数のカラムを選択してクラスにマッピングすることもできます。これは、`User`クラスを定義して、クエリの結果を`User`クラスにマッピングする例です。 - -```scala 3 -case class User(id: Long, name: String, email: String) - -sql"SELECT id, name, email FROM user" - .query[User] // Query[IO, User] - .to[List] // Executor[IO, List[User]] - .readOnly(conn) // IO[List[User]] - .unsafeRunSync() // List[User] - .foreach(println) // Unit -``` - -クラスのフィールドは、クエリのカラム名と一致する必要があります。これは、`User`クラスのフィールドが`id`、`name`、`email`であるため、クエリのカラム名が`id`、`name`、`email`であることを意味します。 - -![Selecting Data](../../img/data_select.png) - -`Join`などを使用して複数のテーブルからデータを選択する方法を見てみましょう。 - -これは、`City`, `Country`, `CityWithCountry`クラスそれぞれを定義して、`city`と`country`テーブルを`Join`したクエリの結果を`CityWithCountry`クラスにマッピングする例です。 - -```scala 3 -case class City(id: Long, name: String) -case class Country(code: String, name: String, region: String) -case class CityWithCountry(coty: City, country: Country) - -sql""" - SELECT - city.id, - city.name, - country.code, - country.name, - country.region - FROM city - JOIN country ON city.country_code = country.code -""" - .query[CityWithCountry] // Query[IO, CityWithCountry] - .to[List] // Executor[IO, List[CityWithCountry]] - .readOnly(conn) // IO[List[CityWithCountry]] - .unsafeRunSync() // List[CityWithCountry] - .foreach(println) // Unit -``` - -クラスのフィールドは、クエリのカラム名と一致する必要があると先ほど述べました。 -この場合、`City`クラスのフィールドが`id`、`name`であり、`Country`クラスのフィールドが`code`、`name`、`region`であるため、クエリのカラム名が`id`、`name`、`code`、`name`、`region`であることを意味します。 - -`Join`を行った場合、それぞれのカラムはテーブル名と共に指定しどのテーブルのカラムかを明示する必要があります。 -この例では、`city.id`、`city.name`、`country.code`、`country.name`、`country.region`として指定しています。 - -ldbcではこのように`テーブル名`.`カラム名`を`クラス名`.`フィールド名`にマッピングすることによって、複数のテーブルから取得したデータをネストしたクラスにマッピングすることができます。 - -![Selecting Data](../../img/data_multi_select.png) - -ldbcでは`Join`を行い複数のテーブルからデータを取得する際に、単体のクラスのみではなくクラスの`Tuple`にマッピングすることもできます。 - -```scala 3 -case class City(id: Long, name: String) -case class Country(code: String, name: String, region: String) - -sql""" - SELECT - city.id, - city.name, - country.code, - country.name, - country.region - FROM city - JOIN country ON city.country_code = country.code -""" - .query[(City, Country)] - .to[List] - .readOnly(conn) - .unsafeRunSync() - .foreach(println) -``` - -この例では、`City`クラスと`Country`クラスを`Tuple`にマッピングしています。 - -ここで注意したいのが、先ほどと異なり`テーブル名`.`カラム名`を`クラス名`.`フィールド名`にマッピングすること際にテーブル名はクラス名を使用しています。 - -そのため、このマッピングには制約がありテーブル名とクラス名は等価でなければいけません。つまり、エイリアスなどを使ってテーブル名を`city`から`c`などに短縮した場合、クラス名も`C`でなければならない。 - -```scala 3 -case class C(id: Long, name: String) -case class CT(code: String, name: String, region: String) - -sql""" - SELECT - c.id, - c.name, - ct.code, - ct.name, - ct.region - FROM city AS c - JOIN country AS ct ON c.country_code = ct.code -""" - .query[(City, Country)] - .to[List] - .readOnly(conn) - .unsafeRunSync() - .foreach(println) -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Setup.md b/docs/src/main/mdoc/ja/tutorial/Setup.md deleted file mode 100644 index 5faa2ae89..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Setup.md +++ /dev/null @@ -1,183 +0,0 @@ -{% - laika.title = セットアップ - laika.metadata.language = ja -%} - -# セットアップ - -素晴らしいldbcの世界へようこそ!このセクションでは、すべてのセットアップをお手伝いします。 - -## データベースセットアップ - -まず、Dockerを使用してデータベースを起動します。以下のコードを使用して、データベースを起動します。 - -```yaml -services: - mysql: - image: mysql:@MYSQL_VERSION@ - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 -``` - -次に、データベースの初期化を行います。 - -以下コードのようにデータベースの作成を行います。 - -```sql -CREATE DATABASE IF NOT EXISTS sandbox_db; -``` - -次に、テーブルの作成を行います。 - -```sql -CREATE TABLE IF NOT EXISTS `user` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(50) NOT NULL, - `email` VARCHAR(100) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS `product` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(100) NOT NULL, - `price` DECIMAL(10, 2) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) - -CREATE TABLE IF NOT EXISTS `order` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `user_id` INT NOT NULL, - `product_id` INT NOT NULL, - `order_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `quantity` INT NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES `user` (id), - FOREIGN KEY (product_id) REFERENCES `product` (id) -) -``` - -それぞれのテーブルにデータを挿入します。 - -```sql -INSERT INTO user (name, email) VALUES - ('Alice', 'alice@example.com'), - ('Bob', 'bob@example.com'), - ('Charlie', 'charlie@example.com'); - -INSERT INTO product (name, price) VALUES - ('Laptop', 999.99), - ('Mouse', 19.99), - ('Keyboard', 49.99), - ('Monitor', 199.99); - -INSERT INTO `order` (user_id, product_id, quantity) VALUES - (1, 1, 1), -- Alice ordered 1 Laptop - (1, 2, 2), -- Alice ordered 2 Mice - (2, 3, 1), -- Bob ordered 1 Keyboard - (3, 4, 1); -- Charlie ordered 1 Monitor -``` - -## Scalaセットアップ - -チュートリアルでは[Scala CLI](https://scala-cli.virtuslab.org/)を使用します。そのため、Scala CLIをインストールする必要があります。 - -```bash -brew install Virtuslab/scala-cli/scala-cli -``` - -**Scala CLIで実行** - -先ほどのデータベースセットアップは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このセットアップを行うことができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -### 最初のプログラム - -はじめに、ldbcを依存関係に持つ新しいプロジェクトを作成します。 - -```scala -//> using scala "@SCALA_VERSION@" -//> using dep "@ORGANIZATION@::ldbc-dsl:@VERSION@" -``` - -ldbcを使う前に、いくつかのシンボルをインポートする必要がある。ここでは便宜上、パッケージのインポートを使用する。これにより、高レベルAPIで作業する際に最もよく使用されるシンボルを得ることができる。 - -```scala -import ldbc.dsl.io.* -``` - -Catsも連れてこよう。 - -```scala -import cats.syntax.all.* -import cats.effect.* -``` - -次に、トレーサーとログハンドラーを提供する。これらは、アプリケーションのログを記録するために使用される。トレーサーは、アプリケーションのトレースを記録するために使用される。ログハンドラーは、アプリケーションのログを記録するために使用される。 - -以下のコードは、トレーサーとログハンドラーを提供するがその実体は何もしない。 - -```scala -given Tracer[IO] = Tracer.noop[IO] -given LogHandler[IO] = LogHandler.noop[IO] -``` - -ldbc高レベルAPIで扱う最も一般的な型は`Executor[F, A]`という形式で、`{java | ldbc}.sql.Connection`が利用可能なコンテキストで行われる計算を指定し、最終的にA型の値を生成します。 - -では、定数を返すだけのExecutorプログラムから始めてみよう。 - -```scala -val program: Executor[IO, Int] = Executor.pure[IO, Int](1) -``` - -次に、データベースに接続するためのコネクタを作成する。コネクタは、データベースへの接続を管理するためのリソースである。コネクタは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 - -※ ここではldbcが独自に作成したコネクタを使用します。コネクタの選択と作成方法は後に説明します。 - -```scala -def connection = Connection[IO]( - host = "127.0.0.1", - port = 13306, - user = "ldbc", - password = Some("password"), - ssl = SSL.Trusted -) -``` - -Executorは、データベースへの接続方法、接続の受け渡し方法、接続のクリーンアップ方法を知っているデータ型であり、この知識によってExecutorをIOへ変換し、実行可能なプログラムを得ることができる。具体的には、実行するとデータベースに接続し、単一のトランザクションを実行するIOが得られる。 - -```scala -connection - .use { conn => - program.readOnly(conn).map(println(_)) - } - .unsafeRunSync() -``` - -万歳!定数を計算できた。これはデータベースに仕事を依頼することはないので、あまり面白いものではないが、最初の一歩が完了です。 - -> この本のコードは、IO.unsafeRunSyncの呼び出し以外はすべて純粋なものであることを覚えておいてほしい。IO.unsafeRunSyncは、通常アプリケーションのエントリー・ポイントにのみ現れる「世界の終わり」の操作である。REPLでは、計算を強制的に "happen "させるためにこれを使用する。 - -**Scala CLIで実行** - -このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Simple-Program.md b/docs/src/main/mdoc/ja/tutorial/Simple-Program.md deleted file mode 100644 index 2cc7d8517..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Simple-Program.md +++ /dev/null @@ -1,106 +0,0 @@ -{% - laika.title = シンプルプログラム - laika.metadata.language = ja -%} - -# シンプルなプログラム - -ここではまず、シンプルなプログラムを作成し実行することでldbcの基本的な使い方を説明します。 - -※ ここで使用するプログラムの環境はセットアップで構築したものを前提としています。 - -## 1つめのプログラム - -このプログラムでは、データベースに接続し計算結果を取得するプログラムを作成します。 - -それでは、`sql string interpolator`を使って、データベースに定数の計算を依頼する問い合わせを作成してみましょう。 - -```scala -val program: Executor[IO, Option[Int]] = sql"SELECT 2".query[Int].to[Option] -``` - -`sql string interpolator`を使って作成したクエリは`query`メソッドで取得する型の決定を行います。ここでは`Int`型を取得するため、`query[Int]`としています。また、`to`メソッドで取得する型を決定します。ここでは`Option`型を取得するため、`to[Option]`としています。 - -| Method | Return Type | Notes | -|--------------|----------------|-------------------------------| -| `to[List]` | `F[List[A]]` | `すべての結果をリストで表示` | -| `to[Option]` | `F[Option[A]]` | `結果は0か1、そうでなければエラーが発生する` | -| `unsafe` | `F[A]` | `正確には1つの結果で、そうでない場合はエラーが発生する` | - -最後に、データベースに接続して値を返すプログラムを書きます。このプログラムは、データベースに接続し、クエリを実行し、結果を取得します。 - -```scala -connection - .use { conn => - program.readOnly(conn).map(println(_)) - } - .unsafeRunSync() -``` - -定数を計算するためにデータベースに接続した。かなり印象的だ。 - -**Scala CLIで実行** - -このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## 2つめのプログラム - -一つの取引で複数のことをしたい場合はどうすればいいのか?簡単だ!Executorはモナドなので、for内包を使って2つの小さなプログラムを1つの大きなプログラムにすることができる。 - -```scala -val program: Executor[IO, (List[Int], Option[Int], Int)] = - for - result1 <- sql"SELECT 1".query[Int].to[List] - result2 <- sql"SELECT 2".query[Int].to[Option] - result3 <- sql"SELECT 3".query[Int].unsafe - yield (result1, result2, result3) -``` - -最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 - -```scala -connection - .use { conn => - program.readOnly(conn).map(println(_)) - } - .unsafeRunSync() -``` - -**Scala CLIで実行** - -このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## 3つめのプログラム - -データベースに対して書き込みを行うプログラムを書いてみよう。ここでは、データベースに接続し、クエリを実行し、データを挿入する。 - -```scala -val program: Executor[IO, Int] = - sql"INSERT INTO user (name, email) VALUES ('Carol', 'carol@example.com')".update -``` - -先ほどと異なる点は、`commit`メソッドを呼び出すことである。これにより、トランザクションがコミットされ、データベースにデータが挿入される。 - -```scala -connection - .use { conn => - program.commit(conn).map(println(_)) - } - .unsafeRunSync() -``` - -**Scala CLIで実行** - -このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Updating-Data.md b/docs/src/main/mdoc/ja/tutorial/Updating-Data.md deleted file mode 100644 index b7b8ac1ad..000000000 --- a/docs/src/main/mdoc/ja/tutorial/Updating-Data.md +++ /dev/null @@ -1,115 +0,0 @@ -{% - laika.title = データ更新 - laika.metadata.language = ja -%} - -# データ更新 - -この章では、データベースのデータを変更する操作と、更新結果を取得する方法について説明します。 - -## 挿入 - -挿入は簡単で、selectと同様に動作します。ここでは、`user`テーブルに行を挿入する`Executor`を作成するメソッドを定義します。 - -```scala -def insertUser(name: String, email: String): Executor[IO, Int] = - sql"INSERT INTO user (name, email) VALUES ($name, $email)" - .update -``` - -行を挿入してみよう。 - -```scala -insertUser("dave", "dave@example.com").commit.unsafeRunSync() -``` - -そして読み返す。 - -```scala -sql"SELECT * FROM user" - .query[(Int, String, String)] // Query[IO, (Int, String, String)] - .to[List] // Executor[IO, List[(Int, String, String)]] - .readOnly(conn) // IO[List[(Int, String, String)]] - .unsafeRunSync() // List[(Int, String, String)] - .foreach(println) // Unit -``` - -## 更新 - -更新も同じパターンだ。ここではユーザーのメールアドレスを更新する。 - -```scala -def updateUserEmail(id: Int, email: String): Executor[IO, Int] = - sql"UPDATE user SET email = $email WHERE id = $id" - .update -``` - -結果の取得 - -```scala -updateUserEmail(1, "alice+1@example.com").commit.unsafeRunSync() - -sql"SELECT * FROM user WHERE id = 1" - .query[(Int, String, String)] // Query[IO, (Int, String, String)] - .to[Option] // Executor[IO, List[(Int, String, String)]] - .readOnly(conn) // IO[List[(Int, String, String)]] - .unsafeRunSync() // List[(Int, String, String)] - .foreach(println) // Unit -// Some((1,alice,alice+1@example.com)) -``` - -## 自動生成キー - -インサートする際には、新しく生成されたキーを返したいものです。まず、挿入して最後に生成されたキーを`LAST_INSERT_ID`で取得し、指定された行を選択するという難しい方法をとります。 - -```scala -def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = - for - _ <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".update - id <- sql"SELECT LAST_INSERT_ID()".query[Int].unsafe - task <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] - yield task -``` - -```scala -insertUser("eve", "eve@example.com").commit.unsafeRunSync() -``` - -これは苛立たしいことだが、すべてのデータベースでサポートされている(ただし、「最後に使用されたIDを取得する」機能はベンダーによって異なる)。 - -MySQLでは、`AUTO_INCREMENT`が設定された行のみが挿入時に返すことができます。上記の操作を2つのステートメントに減らすことができます - -自動生成キーを使用して行を挿入する場合、`returning`メソッドを使用して自動生成キーを取得できます。 - -```scala -def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = - for - id <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".returning[Int] - user <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] - yield user -``` - -```scala -insertUser("frank", "frank@example.com").commit.unsafeRunSync() -``` - -## バッチ更新 - -バッチ更新を行うには、`NonEmptyList`を使用して複数の行を挿入する`insertManyUser`メソッドを定義します。 - -```scala -def insertManyUser(users: NonEmptyList[(String, String)]): Executor[IO, Int] = - val value = users.map { case (name, email) => sql"($name, $email)" } - (sql"INSERT INTO user (name, email) VALUES" ++ values(value)).update -``` - -このプログラムを実行すると、更新された行数が得られる。 - -```scala -val users = NonEmptyList.of( - ("greg", "greg@example.com"), - ("henry", "henry@example.com") -) - -insertManyUser(users).commit.unsafeRunSync() -``` diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf deleted file mode 100644 index a95cc9a92..000000000 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ /dev/null @@ -1,17 +0,0 @@ -laika.title = Tutorial -laika.navigationOrder = [ - index.md, - Setup.md, - Connection.md, - Simple-Program.md, - Database-Operations.md, - Parameterized-Queries.md, - Selecting-Data.md, - Updating-Data.md, - Error-Handling.md, - Logging.md, - Custom-Data-Type.md, - Query-Builder.md, - Schema.md, - Schema-Code-Generation.md -] diff --git a/docs/src/main/mdoc/ja/tutorial/index.md b/docs/src/main/mdoc/ja/tutorial/index.md deleted file mode 100644 index 4ea5ffb27..000000000 --- a/docs/src/main/mdoc/ja/tutorial/index.md +++ /dev/null @@ -1,14 +0,0 @@ -{% - laika.title = はじめに - laika.metadata.language = ja -%} - -# チュートリアル - -このセクションは、あなたが始めるための説明と例を含んでいます。チュートリアル形式で書かれており、最初から最後まで読むことを想定しています。 - -## 目次 - -@:navigationTree { - entries = [ { target = "/ja/tutorial", depth = 2 } ] -} From ab408a32b2002efdd5bbfd29b8fa92a5b01a2bc7 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:18:33 +0900 Subject: [PATCH 149/160] Migrate ja document --- docs/src/main/mdoc/ja/01-Table-Definitions.md | 411 +++++++++++++++ docs/src/main/mdoc/ja/02-Custom-Data-Type.md | 83 +++ .../mdoc/ja/03-Type-safe-Query-Builder.md | 489 ++++++++++++++++++ .../main/mdoc/ja/04-Database-Connection.md | 411 +++++++++++++++ docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md | 39 ++ .../06-Generating-SchemaSPY-Documentation.md | 79 +++ .../main/mdoc/ja/07-Schema-Code-Generation.md | 205 ++++++++ docs/src/main/mdoc/ja/08-Performance.md | 38 ++ docs/src/main/mdoc/ja/directory.conf | 10 +- docs/src/main/mdoc/ja/index.md | 96 +++- 10 files changed, 1831 insertions(+), 30 deletions(-) create mode 100644 docs/src/main/mdoc/ja/01-Table-Definitions.md create mode 100644 docs/src/main/mdoc/ja/02-Custom-Data-Type.md create mode 100644 docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md create mode 100644 docs/src/main/mdoc/ja/04-Database-Connection.md create mode 100644 docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md create mode 100644 docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md create mode 100644 docs/src/main/mdoc/ja/07-Schema-Code-Generation.md create mode 100644 docs/src/main/mdoc/ja/08-Performance.md diff --git a/docs/src/main/mdoc/ja/01-Table-Definitions.md b/docs/src/main/mdoc/ja/01-Table-Definitions.md new file mode 100644 index 000000000..a90f1db88 --- /dev/null +++ b/docs/src/main/mdoc/ja/01-Table-Definitions.md @@ -0,0 +1,411 @@ +{% +laika.title = テーブル定義 +laika.metadata.language = ja +%} + +# テーブル定義 + +この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、[code generator](/ja/07-Schema-Code-Generation.md) を使ってこの作業を省略することもできます。 + +以下のコード例では、以下のimportを想定しています。 + +```scala 3 +import ldbc.core.* +import ldbc.core.attribute.* +``` + +LDBCは、Scalaモデルとデータベースのテーブル定義を1対1のマッピングで管理します。モデルが保持するプロパティとテーブルが保持するカラムのマッピングは、定義順に行われます。テーブル定義は、Create文の構造と非常によく似ています。このため、テーブル定義の構築はユーザーにとって直感的なものとなります。 + +LDBC は、このテーブル定義をさまざまな目的で使用します。型安全なクエリの生成、ドキュメントの生成など。 + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( // CREATE TABLE `user` ( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, + column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL +) // ); +``` + +すべてのカラムはcolumnメソッドで定義されます。各カラムにはカラム名、データ型、属性があります。以下のプリミティブ型が標準でサポートされており、すぐに使用できます。 + +- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` +- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` +- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` +- String +- Boolean +- java.time.* + +Null可能な列は`Option[T]`で表現され、Tはサポートされるプリミティブ型の1つです。Option型でない列はすべてNot Nullであることに注意してください。 + +## データ型 + +モデルが持つプロパティのScala型とカラムが持つデータ型の対応付けは、定義されたデータ型がScala型をサポートしている必要があります。サポートされていない型を割り当てようとするとコンパイルエラーが発生します。 + +データ型がサポートするScalaの型は以下の表の通りです。 + +| Data Type | Scala Type | +|--------------|-------------------------------------------------------------------------------------------------| +| `BIT` | `Byte, Short, Int, Long` | +| `TINYINT` | `Byte, Short` | +| `SMALLINT` | `Short, Int` | +| `MEDIUMINT` | `Int` | +| `INT` | `Int, Long` | +| `BIGINT` | `Long, BigInt` | +| `DECIMAL` | `BigDecimal` | +| `FLOAT` | `Float` | +| `DOUBLE` | `Double` | +| `CHAR` | `String` | +| `VARCHAR` | `String` | +| `BINARY` | `Array[Byte]` | +| `VARBINARY` | `Array[Byte]` | +| `TINYBLOB` | `Array[Byte]` | +| `BLOB` | `Array[Byte]` | +| `MEDIUMBLOB` | `Array[Byte]` | +| `LONGBLOB` | `Array[Byte]` | +| `TINYTEXT` | `String` | +| `TEXT` | `String` | +| `MEDIUMTEXT` | `String` | +| `DATE` | `java.time.LocalDate` | +| `DATETIME` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime` | +| `TIMESTAMP` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime` | +| `TIME` | `java.time.LocalTime` | +| `YEAR` | `java.time.Instant, java.time.LocalDate, java.time.Year` | +| `BOOLEA` | `Boolean` | + +**整数型を扱う際の注意点** + +符号あり、符号なしに応じて、扱えるデータの範囲がScalaの型に収まらないことに注意する必要があります。 + +| Data Type | Signed Range | Unsigned Range | Scala Type | Range | +|-------------|----------------------------------------------|----------------------------|------------------|----------------------------------------------------------------------| +| `TINYINT` | `-128 ~ 127` | `0 ~ 255` | `Byte
Short` | `-128 ~ 127
-32768~32767` | +| `SMALLINT` | `-32768 ~ 32767` | `0 ~ 65535` | `Short
Int` | `-32768~32767
-2147483648~2147483647` | +| `MEDIUMINT` | `-8388608 ~ 8388607` | `0 ~ 16777215` | `Int` | `-2147483648~2147483647` | +| `INT` | `-2147483648 ~ 2147483647` | `0 ~ 4294967295` | `Int
Long` | `-2147483648~2147483647
-9223372036854775808~9223372036854775807` | +| `BIGINT` | `-9223372036854775808 ~ 9223372036854775807` | `0 ~ 18446744073709551615` | `Long
BigInt` | `-9223372036854775808~9223372036854775807
...` | + +ユーザー定義の独自型やサポートされていない型を扱う場合は、[カスタム型](/ja/02-Custom-Data-Type.md) を参照してください。 + +## 属性 + +カラムにはさまざまな属性を割り当てることができます。 + +- `AUTO_INCREMENT` + DDL文を作成し、SchemaSPYを文書化する際に、列を自動インクリメント・キーとしてマークする。 + MySQLでは、データ挿入時にAutoIncでないカラムを返すことはできません。そのため、必要に応じて、LDBCは戻りカラムがAutoIncとして適切にマークされているかどうかを確認します。 +- `PRIMARY_KEY` + DDL文やSchemaSPYドキュメントを作成する際に、列を主キーとしてマークする。 +- `UNIQUE_KEY` + DDL文やSchemaSPYドキュメントを作成する際に、列を一意キーとしてマークする。 +- `COMMENT` + DDL文やSchemaSPY文書を作成する際に、列にコメントを設定する。 + +## キーの設定 + +MySQLではテーブルに対してUniqueキーやIndexキー、外部キーなどの様々なキーを設定することができます。LDBCで構築したテーブル定義でこれらのキーを設定する方法を見ていきましょう。 + +### PRIMARY KEY + +主キー(primary key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムにプライマリーキー制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。また NULL も格納することができません。その結果、プライマリーキー制約が設定されたカラムの値を検索することで、テーブルの中でただ一つのデータを特定することができます。 + +LDBCではこのプライマリーキー制約を2つの方法で設定することができます。 + +1. columnメソッドの属性として設定する +2. tableのkeySetメソッドで設定する + +**columnメソッドの属性として設定する** + +columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`PRIMARY_KEY`を渡すだけです。これによって以下の場合 `id`カラムを主キーとして設定することができます。 + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) +``` + +**tableのkeySetメソッドで設定する** + +LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`PRIMARY_KEY`に主キーとして設定したいカラムを渡すことで主キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => PRIMARY_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`) +// ) +``` + +`PRIMARY_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 + +- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH +- `Index Option` ldbc.core.Index.IndexOption + +#### 複合キー (primary key) + +1つのカラムだけではなく、複数のカラムを主キーとして組み合わせ主キーとして設定することもできます。`PRIMARY_KEY`に主キーとして設定したいカラムを複数渡すだけで複合主キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => PRIMARY_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`, `name`) +// ) +``` + +複合キーは`keySet`メソッドでの`PRIMARY_KEY`でしか設定することはできません。仮に以下のようにcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを主キーとして設定されてしまいます。 + +LDBCではテーブル定義に複数`PRIMARY_KEY`を設定したとしてもコンパイルエラーにすることはできません。しかし、テーブル定義をクエリの生成やドキュメントの生成などで使用する場合エラーとなります。これはPRIMARY KEYはテーブルごとに1つしか設定することができないという制約によるものです。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255), PRIMARY_KEY), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + +// CREATE TABLE `user` ( +// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, +// ) +``` + +### UNIQUE KEY + +一意キー(unique key)とはMySQLにおいてデータを一意に識別するための項目のことです。カラムに一意性制約を設定すると、カラムには他のデータの値を重複することのない値しか格納することができなくなります。 + +LDBCではこの一意性制約を2つの方法で設定することができます。 + +1. columnメソッドの属性として設定する +2. tableのkeySetメソッドで設定する + +**columnメソッドの属性として設定する** + +columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`UNIQUE_KEY`を渡すだけです。これによって以下の場合 `id`カラムを一意キーとして設定することができます。 + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) +``` + +**tableのkeySetメソッドで設定する** + +LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`UNIQUE_KEY`に一意キーとして設定したいカラムを渡すことで一意キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => UNIQUE_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`) +// ) +``` + +`UNIQUE_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 + +- `Index Name` String +- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH +- `Index Option` ldbc.core.Index.IndexOption + +#### 複合キー (unique key) + +1つのカラムだけではなく、複数のカラムを一意キーとして組み合わせ一意キーとして設定することもできます。`UNIQUE_KEY`に一意キーとして設定したいカラムを複数渡すだけで複合一意キーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => UNIQUE_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`, `name`) +// ) +``` + +複合キーは`keySet`メソッドでの`UNIQUE_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合キーとしてではなく、それぞれを一意キーとして設定されてしまいます。 + +### INDEX KEY + +インデックスキー(index key)とはMySQLにおいて目的のレコードを効率よく取得するための「索引」のことです。 + +LDBCではこのインデックスを2つの方法で設定することができます。 + +1. columnメソッドの属性として設定する +2. tableのkeySetメソッドで設定する + +**columnメソッドの属性として設定する** + +columnメソッドの属性として設定する方法は非常に簡単で、columnメソッドの第3引数以降に`INDEX_KEY`を渡すだけです。これによって以下の場合 `id`カラムをインデックスとして設定することができます。 + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) +``` + +**tableのkeySetメソッドで設定する** + +LDBCのテーブル定義には `keySet`というメソッドが生えており、ここで`INDEX_KEY`にインデックスとして設定したいカラムを渡すことでインデックスキーとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => INDEX_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`) +// ) +``` + +`INDEX_KEY`メソッドにはカラム意外にも以下のパラメーターを設定することができます。 + +- `Index Name` String +- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH +- `Index Option` ldbc.core.Index.IndexOption + +#### 複合キー (index key) + +1つのカラムだけではなく、複数のカラムをインデックスキーとして組み合わせインデックスキーとして設定することもできます。`INDEX_KEY`にインデックスキーとして設定したいカラムを複数渡すだけで複合インデックスとして設定することができます。 + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => INDEX_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`, `name`) +// ) +``` + +複合キーは`keySet`メソッドでの`INDEX_KEY`でしか設定することはできません。仮にcolumnメソッドの属性として複数設定を行うと複合インデックスとしてではなく、それぞれをインデックスキーとして設定されてしまいます。 + +### FOREIGN KEY + +外部キー(foreign key)とは、MySQLにおいてデータの整合性を保つための制約(参照整合性制約)です。 外部キーに設定されているカラムには、参照先となるテーブルのカラム内に存在している値しか設定できません。 + +LDBCではこの外部キー制約をtableのkeySetメソッドを使用する方法で設定することができます。 + +```scala 3 +val post = Table[Post]("post")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)) +) + +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// ) +``` + +`FOREIGN_KEY`メソッドにはカラムとReference値意外にも以下のパラメーターを設定することができます。 + +- `Index Name` String + +外部キー制約には親テーブルの削除時と更新時の挙動を設定することができます。`REFERENCE`メソッドに`onDelete`と`onUpdate`メソッドが提供されているのでこちらを使用することでそれぞれ設定することができます。 + +設定することのできる値は`ldbc.core.Reference.ReferenceOption`から取得することができます。 + +```scala 3 +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id).onDelete(Reference.ReferenceOption.RESTRICT))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT +// ) +``` + +設定することのできる値は以下になります。 + +- `RESTRICT`: 親テーブルに対する削除または更新操作を拒否します。 +- `CASCADE`: 親テーブルから行を削除または更新し、子テーブル内の一致する行を自動的に削除または更新します。 +- `SET_NULL`: 親テーブルから行を削除または更新し、子テーブルの外部キーカラムを NULL に設定します。 +- `NO_ACTION`: 標準 SQL のキーワード。 MySQLでは、RESTRICT と同等です。 +- `SET_DEFAULT`: このアクションは MySQL パーサーによって認識されますが、InnoDB と NDB はどちらも、ON DELETE SET DEFAULT または ON UPDATE SET DEFAULT 句を含むテーブル定義を拒否します。 + +#### 複合キー (foreign key) + +1つのカラムだけではなく、複数のカラムを外部キーとして組み合わせて設定することもできます。`FOREIGN_KEY`に外部キーとして設定したいカラムを複数渡すだけで複合外部キーとして設定することができます。 + +```scala 3 +val post = Table[Post]("post")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("category", SMALLINT[Short]) +) + +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]), + column("post_category", SMALLINT[Short]) +) + .keySet(table => FOREIGN_KEY((table.postId, table.postCategory), REFERENCE(post, (post.id, post.category)))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`, `post_category`) REFERENCES `post` (`id`, `category`) +// ) +``` + +### 制約名 + +MySQLではCONSTRAINTを使用することで制約に対して任意の名前を付与することができます。この制約名はデータベース単位で一意の値である必要があります。 + +LDBCではCONSTRAINTメソッドが提供されているのでキー制約などの制約を設定する処理をCONSTRAINTメソッドに渡すだけで設定することができます。 + +```scala 3 +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => CONSTRAINT("fk_post_id", FOREIGN_KEY(table.postId, REFERENCE(post, post.id)))) + +// CREATE TABLE `user` ( +// ..., +// CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// ) +``` diff --git a/docs/src/main/mdoc/ja/02-Custom-Data-Type.md b/docs/src/main/mdoc/ja/02-Custom-Data-Type.md new file mode 100644 index 000000000..cffb7ceef --- /dev/null +++ b/docs/src/main/mdoc/ja/02-Custom-Data-Type.md @@ -0,0 +1,83 @@ +{% +laika.title = カスタム データ型 +laika.metadata.language = ja +%} + +# カスタム データ型 + +この章では、LDBCで構築したテーブル定義でユーザー独自の型もしくはサポートされていない型を使用するための方法について説明します。 + +以下のコード例では、以下のimportを想定しています。 + +```scala 3 +import ldbc.core.* +``` + +ユーザー独自の型もしくはサポートされていない型を使用するための方法はカラムのデータ型をどのような型として扱うかを教えてあげることです。DataTypeには`mapping`メソッドが提供されているのでこのメソッドを使用して暗黙の型変換として設定します。 + +```scala 3 +case class User( + id: Long, + name: User.Name, + age: Option[Int], +) + +object User: + + case class Name(firstName: String, lastName: String) + + given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` + +LDBCでは複数のカラムをモデルが持つ1つのプロパティに統合することはできません。LDBCの目的はモデルとテーブルを1対1でマッピングを行い、データベースのテーブル定義を型安全に構築することにあるからです。 + +そのためテーブル定義とモデルで異なった数のプロパティを持つようなことは許可していません。以下のような実装はコンパイルエラーとなります。 + +```scala 3 +case class User( + id: Long, + name: User.Name, + age: Option[Int], +) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("first_name", VARCHAR(255)), + column("last_name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` + +上記のような実装を行いたい場合は以下のような実装を検討してください。 + +```scala 3 +case class User( + id: Long, + firstName: String, + lastName: String, + age: Option[Int], +): + + val name: User.Name = User.Name(firstName, lastName) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("first_name", VARCHAR(255)), + column("last_name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` diff --git a/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md b/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md new file mode 100644 index 000000000..aeb455c40 --- /dev/null +++ b/docs/src/main/mdoc/ja/03-Type-safe-Query-Builder.md @@ -0,0 +1,489 @@ +{% +laika.title = 型安全なクエリ構築 +laika.metadata.language = ja +%} + +# 型安全なクエリ構築 + +この章では、LDBCで構築したテーブル定義を使用して、型安全にクエリを構築するための方法について説明します。 + +プロジェクトに以下の依存関係を設定する必要があります。 + +```scala +libraryDependencies += "@ORGANIZATION@" %% "ldbc-query-builder" % "@VERSION@" +``` + +LDBCでのテーブル定義方法をまだ読んでいない場合は、[テーブル定義](/ja/01-Table-Definitions.md)の章を先に読むことをオススメしましす。 + +以下のコード例では、以下のimportを想定しています。 + +```scala 3 +import cats.effect.IO +import ldbc.core.* +import ldbc.query.builder.TableQuery +``` + +LDBCではTableQueryにテーブル定義を渡すことで型安全なクエリ構築を行います。 + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), +) + +val userQuery = TableQuery[User](table) +``` + +## SELECT + +型安全にSELECT文を構築する方法はTableQueryが提供する`select`メソッドを使用することです。LDBCではプレーンなクエリに似せて実装されているため直感的にクエリ構築が行えます。またどのようなクエリが構築されているかも一目でわかるような作りになっています。 + +特定のカラムのみ取得を行うSELECT文を構築するには`select`メソッドで取得したいカラムを指定するだけです。 + +```scala 3 +val select = userQuery.select(_.id) + +select.statement === "SELECT `id` FROM user" +``` + +複数のカラムを指定する場合は`select`メソッドで取得したいカラムを指定して指定したカラムのタプルを返すだけです。 + +```scala 3 +val select = userQuery.select(user => (user.id, user.name)) + +select.statement === "SELECT `id`, `name` FROM user" +``` + +全てのカラムを指定したい場合はTableQueryが提供する`selectAll`メソッドを使用することで構築できます。 + +```scala 3 +val select = userQuery.selectAll + +select.statement === "SELECT `id`, `name`, `age` FROM user" +``` + +特定のカラムの数を取得したい場合は、指定したカラムで`count`を使用することで構築できます。  + +```scala 3 +val select = userQuery.select(_.id.count) + +select.statement === "SELECT COUNT(id) FROM user" +``` + +### WHERE + +クエリに型安全にWhere条件を設定する方法は`where`メソッドを使用することです。 + +```scala 3 +val select = userQuery.select(_.id).where(_.name === "Test") + +select.statement === "SELECT `id` FROM user WHERE name = ?" +``` + +`where`メソッドで使用できる条件の一覧は以下です。 + +| 条件 | ステートメント | +|----------------------------------------|---------------------------------------| +| `===` | `column = ?` | +| `>=` | `column >= ?` | +| `>` | `column > ?` | +| `<=` | `column <= ?` | +| `<` | `column < ?` | +| `<>` | `column <> ?` | +| `!==` | `column != ?` | +| `IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL")` | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | +| `<=>` | `column <=> ?` | +| `IN (value, value, ...)` | `column IN (?, ?, ...)` | +| `BETWEEN (start, end)` | `column BETWEEN ? AND ?` | +| `LIKE (value)` | `column LIKE ?` | +| `LIKE_ESCAPE (like, escape)` | `column LIKE ? ESCAPE ?` | +| `REGEXP (value)` | `column REGEXP ?` | +| `<<` (value) | `column << ?` | +| `>>` (value) | `column >> ?` | +| `DIV (cond, result)` | `column DIV ? = ?` | +| `MOD (cond, result)` | `column MOD ? = ?` | +| `^ (value)` | `column ^ ?` | +| `~ (value)` | `~column = ?` | + +### GROUP BY/Having + +クエリに型安全にGROUP BY句を設定する方法は`groupBy`メソッドを使用することです。 + +`groupBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準にグループ化することができます。 + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3) + +select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age" +``` + +グループ化すると`select`で取得できるデータの数はグループの数だけとなります。そこでグループ化を行った場合には、グループ化に指定したカラムの値や、用意された関数を使ってカラムの値をグループ単位で集計した結果などを取得することができます。 + +`having`を使用すると`groupBy`によってグループ化されて取得したデータに関して、取得する条件を設定することができます。 + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3).having(_._3 > 20) + +select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age HAVING age > ?" +``` + +### ORDER BY + +クエリに型安全にORDER BY句を設定する方法は`orderBy`メソッドを使用することです。 + +`orderBy`を使うことで`select`でデータを取得する時に指定したカラムの値を対象にソートした結果を取得することができます。 + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age) + +select.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age" +``` + +昇順/降順を指定したい場合は、それぞれカラムに対して `asc`/`desc`を呼び出すだけです。 + +```scala 3 +val desc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.desc) + +desc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age DESC" + +val asc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.asc) + +asc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age ASC" +``` + +### LIMIT/OFFSET + +クエリに型安全にLIMIT句とOFFSET句を設定する方法は`limit`/`offset`メソッドを使用することです。 + +`limit`を設定すると`select`を実行した時に取得するデータの行数の上限を設定することができ、`offset`を設定すると何番目からのデータを取得するのかを指定することができます。 + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).limit(100).offset(50) + +select.statement === "SELECT `id`, `name`, `age` FROM user LIMIT ? OFFSET ?" +``` + +## JOIN/LEFT JOIN/RIGHT JOIN + +クエリに型安全にJoinを設定する方法は`join`/`leftJoin`/`rightJoin`メソッドを使用することです。 + +Joinでは以下定義をサンプルとして使用します。 + +```scala 3 +case class Country(code: String, name: String) +object Country: + val table = Table[Country]("country")( + column("code", CHAR(3), PRIMARY_KEY), + column("name", VARCHAR(255)) + ) + +case class City(id: Long, name: String, countryCode: String) +object City: + val table = Table[City]("city")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("country_code", CHAR(3)) + ) + +case class CountryLanguage( + countryCode: String, + language: String +) +object CountryLanguage: + val table: Table[CountryLanguage] = Table[CountryLanguage]("country_language")( + column("country_code", CHAR(3)), + column("language", CHAR(30)) + ) + +val countryQuery = TableQuery[Country](Country.table) +val cityQuery = TableQuery[City](City.table) +val countryLanguageQuery = TableQuery[CountryLanguage](CountryLanguage.table) +``` + +まずシンプルなJoinを行いたい場合は、`join`を使用します。 +`join`の第一引数には結合したいテーブルを渡し、第二引数では結合元のテーブルと結合したいテーブルのカラムで比較を行う関数を渡します。これはJoinにおいてのON句に該当します。 + +Join後の`select`は2つのテーブルからカラムを指定することになります。 + +```scala 3 +val join = countryQuery.join(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country JOIN city ON country.code = city.country_code" +``` + +次に左外部結合であるLeft Joinを行いたい場合は、`leftJoin`を使用します。 +`join`が`leftJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 + +```scala 3 +val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city ON country.code = city.country_code" +``` + +シンプルなJoinとの違いは`leftJoin`を使用した場合、結合を行うテーブルから取得するレコードはNULLになる可能性があるということです。 + +そのためLDBCでは`leftJoin`に渡されたテーブルから取得するカラムのレコードは全てOption型になります。 + +```scala 3 +val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) // (String, Option[String]) +``` + +次に右外部結合であるRight Joinを行いたい場合は、`rightJoin`を使用します。 +こちらも`join`が`rightJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 + +```scala 3 +val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN city ON country.code = city.country_code" +``` + +シンプルなJoinとの違いは`rightJoin`を使用した場合、結合元のテーブルから取得するレコードはNULLになる可能性があるということです。 + +そのためLDBCでは`rightJoin`を使用した結合元のテーブルから取得するカラムのレコードは全てOption型になります。 + +```scala 3 +val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) // (Option[String], String) +``` + +複数のJoinを行いたい場合は、メソッドチェーンで任意のJoinメソッドを呼ぶことで実現することができます。 + +```scala 3 +val join = + (countryQuery join cityQuery)((country, city) => country.code === city.countryCode) + .rightJoin(countryLanguageQuery)((_, city, countryLanguage) => city.countryCode === countryLanguage.countryCode) + .select((country, city, countryLanguage) => (country.name, city.name, countryLanguage.language)) // (Option[String], Option[String], String)] + +join.statement = + """ + |SELECT + | country.`name`, + | city.`name`, + | country_language.`language` + |FROM country + |JOIN city ON country.code = city.country_code + |RIGHT JOIN country_language ON city.country_code = country_language.country_code + |""".stripMargin +``` + +複数のJoinを行っている状態で`rightJoin`での結合を行うと、今までの結合が何であったかにかかわらず直前まで結合していたテーブルから取得するレコードは全てNULL許容なアクセスとなることに注意してください。 + +## Custom Data Type + +前章でユーザー独自の型もしくはサポートされていない型を使用するためにDataTypeの`mapping`メソッドを使用して独自の型とDataTypeのマッピングを行ないました。([参照](/ja/02-Custom-Data-Type.md)) + +LDBCはテーブル定義とデータベースへの接続処理が分離されています。 +そのためデータベースからデータを取得する際にユーザー独自の型もしくはサポートされていない型に変換したい場合は、ResultSetからのデータ取得方法を独自の型もしくはサポートされていない型と紐付けてあげる必要があります。 + +例えばユーザー定義のEnumを文字列型とマッピングしたい場合は、以下のようになります。 + +```scala 3 +enum Custom: + case ... + +given ResultSetReader[IO, Custom] = + ResultSetReader.mapping[IO, str, Custom](str => Custom.valueOf(str)) +``` + +※ この処理は将来のバージョンでDataTypeのマッピングと統合される可能性があります。 + +## INSERT + +型安全にINSERT文を構築する方法はTableQueryが提供する以下のメソッドを使用することです。 + +- insert +- insertInto +- += +- ++= + +**insert** + +`insert`メソッドには挿入するデータのタプルを渡します。タプルはモデルと同じプロパティの数と型である必要があります。また、挿入されるデータの順番はモデルのプロパティおよびテーブルのカラムと同じ順番である必要があります。 + +```scala 3 +val insert = userQuery.insert((1L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" +``` + +複数のデータを挿入したい場合は、`insert`メソッドに複数のタプルを渡すことで構築できます。 + +```scala 3 +val insert = userQuery.insert((1L, "name", None), (2L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +**insertInto** + +`insert`メソッドはテーブルが持つ全てのカラムにデータ挿入を行いますが、特定のカラムに対してのみデータを挿入したい場合は`insertInto`メソッドを使用します。 + +これはAutoIncrementやDefault値を持つカラムへのデータ挿入を除外したい場合などに使用できます。 + +```scala 3 +val insert = userQuery.insertInto(user => (user.name, user.age)).values(("name", None)) + +insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?)" +``` + +複数のデータを挿入したい場合は、`values`にタプルの配列を渡すことで構築できます。 + +```scala 3 +val insert = userQuery.insertInto(user => (user.name, user.age)).values(List(("name", None), ("name", Some(20)))) + +insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?), (?, ?)" +``` + +**+=** + +`+=`メソッドを使用することでモデルを使用してinsert文を構築することができます。モデルを使用する場合は全てのカラムにデータを挿入してしまうことに注意してください。 + +```scala 3 +val insert = userQuery += User(1L, "name", None) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" +``` + +**++=** + +モデルを使用して複数のデータを挿入したい場合は`++=`メソッドを使用します。 + +```scala 3 +val insert = userQuery ++= List(User(1L, "name", None), User(2L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +### ON DUPLICATE KEY UPDATE + +ON DUPLICATE KEY UPDATE 句を指定し行を挿入すると、UNIQUEインデックスまたはPRIMARY KEYで値が重複する場合、古い行のUPDATEが発生します。 + +LDBCでこの処理を実現する方法は2種類あり、`insertOrUpdate{s}`を使用するか、`Insert`に対して`onDuplicateKeyUpdate`を使用することです。 + +```scala 3 +val insert = userQuery.insertOrUpdate((1L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `id` = new_user.`id`, `name` = new_user.`name`, `age` = new_user.`age`" +``` + +`insertOrUpdate{s}`を使用した場合、全てのカラムが更新対象となることに注意してください。重複する値があり特定のカラムのみを更新したい場合は、`onDuplicateKeyUpdate`を使用して更新したいカラムのみを指定するようにしてください。 + +```scala 3 +val insert = userQuery.insert((1L, "name", None)).onDuplicateKeyUpdate(v => (v.name, v.age)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `age` = new_user.`age`" +``` + +## UPDATE + +型安全にUPDATE文を構築する方法はTableQueryが提供する`update`メソッドを使用することです。 + +`update`メソッドの第1引数にはテーブルのカラム名ではなくモデルのプロパティ名を指定し、第2引数に更新したい値を渡します。第2引数に渡す値の型は第1引数で指定したプロパティの型と同じである必要があります。 + +```scala 3 +val update = userQuery.update("name", "update name") + +update.statement === "UPDATE user SET name = ?" +``` + +第1引数に存在しないプロパティ名を指定した場合コンパイルエラーとなります。 + +```scala 3 +val update = userQuery.update("hoge", "update name") // Compile error +``` + +複数のカラムを更新したい場合は`set`メソッドを使用します。 + +```scala 3 +val update = userQuery.update("name", "update name").set("age", Some(20)) + +update.statement === "UPDATE user SET name = ?, age = ?" +``` + +`set`メソッドには条件に応じてクエリを生成させないようにすることもできます。 + +```scala 3 +val update = userQuery.update("name", "update name").set("age", Some(20), false) + +update.statement === "UPDATE user SET name = ?" +``` + +モデルを使用してupdate文を構築することもできます。モデルを使用する場合は全てのカラムを更新してしまうことに注意してください。 + +```scala 3 +val update = userQuery.update(User(1L, "update name", None)) + +update.statement === "UPDATE user SET id = ?, name = ?, age = ?" +``` + +### WHERE + +`where`メソッドを使用することでupdate文にもWhere条件を設定することができます。 + +```scala 3 +val update = userQuery.update("name", "update name").set("age", Some(20)).where(_.id === 1) + +update.statement === "UPDATE user SET name = ?, age = ? WHERE id = ?" +``` + +`where`メソッドで使用できる条件はSelect文の[where項目](/ja/03-Type-safe-Query-Builder.md)を参照してください。 + +## DELETE + +型安全にDELETE文を構築する方法はTableQueryが提供する`delete`メソッドを使用することです。 + +```scala 3 +val delete = userQuery.delete + +delete.statement === "DELETE FROM user" +``` + +### WHERE + +`where`メソッドを使用することでdelete文にもWhere条件を設定することができます。 + +```scala 3 +val delete = userQuery.delete.where(_.id === 1) + +delete.statement === "DELETE FROM user WHERE id = ?" +``` + +`where`メソッドで使用できる条件はSelect文の[where項目](/ja/03-Type-safe-Query-Builder.md)を参照してください。 + +## DDL + +型安全にDDLを構築する方法はTableQueryが提供する以下のメソッドを使用することです。 + +- createTable +- dropTable +- truncateTable + +spec2を使用している場合は以下のようにしてテストの前後にDDLを実行することができます。 + +```scala 3 +import cats.effect.IO +import cats.effect.unsafe.implicits.global + +import org.specs2.mutable.Specification +import org.specs2.specification.core.Fragments +import org.specs2.specification.BeforeAfterEach + +object Test extends Specification, BeforeAfterEach: + + override def before: Fragments = + step((tableQuery.createTable.update.autoCommit(dataSource) >> IO.println("Complete create table")).unsafeRunSync()) + + override def after: Fragments = + step((tableQuery.dropTable.update.autoCommit(dataSource) >> IO.println("Complete drop table")).unsafeRunSync()) +``` diff --git a/docs/src/main/mdoc/ja/04-Database-Connection.md b/docs/src/main/mdoc/ja/04-Database-Connection.md new file mode 100644 index 000000000..f8516f835 --- /dev/null +++ b/docs/src/main/mdoc/ja/04-Database-Connection.md @@ -0,0 +1,411 @@ +{% +laika.title = データベース接続 +laika.metadata.language = ja +%} + +# データベース接続 + +この章では、LDBCで構築したクエリを使用して、データベースへの接続処理を行うための方法について説明します。 + +プロジェクトに以下の依存関係を設定する必要があります。 + +```scala +libraryDependencies ++= Seq( + "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", + "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" +) +``` + +LDBCでのクエリ構築方法をまだ読んでいない場合は、[型安全なクエリ構築](/ja/03-Type-safe-Query-Builder.md)の章を先に読むことをオススメしましす。 + +以下のコード例では、以下のimportを想定しています。 + +```scala 3 +import com.mysql.cj.jdbc.MysqlDataSource + +import cats.effect.IO +// This is just for testing. Consider using cats.effect.IOApp instead of calling +// unsafe methods directly. +import cats.effect.unsafe.implicits.global + +import ldbc.sql.* +import ldbc.dsl.io.* +import ldbc.dsl.logging.ConsoleLogHandler +import ldbc.query.builder.TableQuery +``` + +テーブル定義は以下を使用します。 + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), +) + +val userQuery = TableQuery[User](table) +``` + +## DataSourceの使用 + +LDBCはデータベース接続にJDBCのDataSourceを使用します。LDBCにはこのDataSourceを構築する実装は提供されていないため、mysqlやHikariCPなどのライブラリを使用する必要があります。今回の例ではMysqlDataSourceを使用してDataSourceの構築を行います。 + +```scala 3 +private val dataSource = new MysqlDataSource() +dataSource.setServerName("127.0.0.1") +dataSource.setPortNumber(3306) +dataSource.setDatabaseName("database name") +dataSource.setUser("user name") +dataSource.setPassword("password") +``` + +## ログ + +LDBCではDatabase接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 + +標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 + +```scala 3 +given LogHandler[IO] = ConsoleLogHandler[IO] +``` + +### カスタマイズ + +任意のロギングライブラリを使用してログをカスタマイズする場合は`ldbc.dsl.logging.LogHandler`を使用します。 + +以下は標準実装のログ実装です。LDBCではデータベース接続で以下3種類のイベントが発生します。 + +- Success: 処理の成功 +- ProcessingFailure: データ取得後もしくはデータベース接続前の処理のエラー +- ExecFailure: データベースへの接続処理のエラー + +それぞれのイベントでどのようなログを書き込むかをパターンマッチングによって振り分けを行います。 + +```scala 3 +def consoleLogger[F[_]: Console: Sync]: LogHandler[F] = + case LogEvent.Success(sql, args) => + Console[F].println( + s"""Successful Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) + case LogEvent.ProcessingFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed ResultSet Processing: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) + case LogEvent.ExecFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) +``` + +## Query + +`select`文を構築すると`toList`/`headOption`/`unsafe`メソッドを使用できるようになります。これらのメソッドは取得後のデータ形式を決定するために使用します。特段何も型を指定しない場合は`select`メソッドで指定したカラムの型がTupleとして返却されます。 + +### toList + +クエリを実行した結果データの一覧を取得したい場合は、`toList`メソッドを使用します。`toList`メソッドを使用してデータベース処理を行なった結果、データ取得件数が0件であった場合空の配列が返されます。 + +```scala 3 +val query1 = userQuery.selectAll.toList // List[(Long, String, Option[Int])] +``` + +`toList`メソッドにモデルを指定すると取得後のデータを指定したモデルに変換することができます。 + +```scala 3 +val query = userQuery.selectAll.toList[User] // User +``` + +`toList`メソッドで指定するモデルの型は`select`メソッドで指定したTupleの型と一致するか、Tupleの型から指定したモデルへの型変換が可能なものでなければなりません。 + +```scala 3 +val query1 = userQuery.select(user => (user.name, user.age)).toList[User] // Compile error + +case class Test(name: String, age: Option[Int]) +val query2 = userQuery.select(user => (user.name, user.age)).toList[Test] // Test +``` + +### headOption + +クエリを実行した結果最初の1件のデータをOptionalで取得したい場合は、`headOption`メソッドを使用します。`headOption`メソッドを使用してデータベース処理を行なった結果データ取得件数が0件であった場合Noneが返されます。 + +`headOption`メソッドを使用した場合、複数のデータを取得するクエリを実行したとしても最初のデータのみ返されることに注意してください。 + +```scala 3 +val query1 = userQuery.selectAll.headOption // Option[(Long, String, Option[Int])] +val query2 = userQuery.selectAll.headOption[User] // Option[User] +``` + +### unsafe + +`unsafe`メソッドを使用した場合、取得したデータの最初の1件のみ返されることは`headOption`メソッドと同じですが、データはOptionalにはならずそのままのデータが返却されます。もし取得したデータの件数が0件であった場合は例外が発生するため適切な例外ハンドリングを行う必要があります。 + +実行時に例外を発生する可能性が高いため`unsafe`という名前になっています。 + +```scala 3 +val query1 = userQuery.selectAll.unsafe // (Long, String, Option[Int]) +val query2 = userQuery.selectAll.unsafe[User] // User +``` + +## Update + +`insert/update/delete`文を構築すると`update`メソッドを使用できるようになります。`update`メソッドはデータベースへの書き込み処理件数を返却します。 + +```scala 3 +val insert = userQuery.insert((1L, "name", None)).update // Int +val update = userQuery.update("name", "update name").update // Int +val delete = userQuery.delete.update // Int +``` + +`insert`文の場合データ挿入時にAutoIncrementで生成された値を返却させたい場合があります。その場合は`update`メソッドではなく`returning`メソッドを使用して返却したいカラムを指定します。 + +```scala 3 +val insert = userQuery.insert((1L, "name", None)).returning("id") // Long +``` + +`returning`メソッドで指定する値はモデルが持つプロパティ名である必要があります。また、指定したプロパティがテーブル定義上でAutoIncrementの属性が設定されていなければエラーとなってしまいます。 + +MySQLではデータ挿入時に返却できる値はAutoIncrementのカラムのみであるため、LDBCでも同じような仕様となっています。 + +## データベース操作の実行 + +データベース接続を行う前にコミットのタイミングや読み書き専用などの設定を行う必要があります。 + +### 読み取り専用 + +`readOnly`メソッドを使用することで実行するクエリの処理を読み込み専用にすることができます。`readOnly`メソッドは`insert/update/delete`文でも使用することができますが、書き込み処理を行うので実行時にエラーとなります。 + +```scala 3 +val read = userQuery.selectAll.toList.readOnly(dataSource) +``` + +### 自動コミット + +`autoCommit`メソッドを使用することで実行するクエリの処理をクエリ実行時ごとにコミットするように設定することができます。 + +```scala 3 +val read = userQuery.insert((1L, "name", None)).update.autoCommit(dataSource) +``` + +### トランザクション + +`transaction`メソッドを使用することで複数のデータベース接続処理を1つのトランザクションにまとめることができます。 + +`toList/headOption/unsafe/returning/update`メソッドの戻り値は`Kleisli[F, Connection[F], T]`型となっています。そのためmapやflatMapを使用して処理を1つにまとめることができます。 + +1つにまとめた`Kleisli[F, Connection[F], T]`に対して`transaction`メソッドを使用することで、中で行われる全てのデータベース接続処理は1つのトランザクションにまとめて実行されます。 + +```scala 3 +(for + result1 <- userQuery.insert((1L, "name", None)).returning("id") + result2 <- userQuery.update("name", "update name").update + ... +yield ...).transaction(dataSource) +``` + +## Database Action + +データベース処理を実行する方法としてデータベースへの接続情報を持った`Database`を使用して行う方法も存在します。 + +`Database`を構築する方法はDriverManagerを使用した方法と、DataSourceから生成する方法の2種類があります。以下はMySQLのドライバーを使用してデータベースへの接続情報を持った`Database`を構築する例です。 + +```scala 3 +val db = Database.fromMySQLDriver[IO]("database name", "host", "port number", "user name", "password") +``` + +`Database`を使用してデータベース処理を実行するメリットは以下になります。 + +- DataSourceの構築を簡略できる (DriverManagerを使用した場合) +- クエリごとにDataSourceを受け渡す必要がなくなる + +`Database`を使用する方法は、DataSourceを受け渡す方法を簡略化しただけにすぎないため、どちらを使用しても実行結果に差が出ることはありません。 +`flatMap`などで処理を結合しメソッドチェーンで実行するか、結合した処理を`Database`を使用して実行するかの違いでしかありません。そのため実行方法はユーザーの好きの方法を選択できます。 + +**Read Only** + +```scala 3 +val user: Option[User] = db.readOnly(userQuery.selectAll.headOption[User]).unsafeRunSync() +``` + +**Auto Commit** + +```scala 3 +val result = db.autoCommit(userQuery.insert((1L, "name", None)).update).unsafeRunSync() +``` + +**Transaction** + +```scala 3 +db.transaction(for + result1 <- userQuery.insert((1L, "name", None)).returning("id") + result2 <- userQuery.update("name", "update name").update + ... +yield ...).unsafeRunSync() +``` + +### Database model + +LDBCでは`Database`モデルはデータベースの接続情報を持つ以外の用途でも使用されます。他の用途としてSchemaSPYのドキュメント生成に使用されることです。SchemaSPYのドキュメント生成に関しては[こちら](/ja/06-Generating-SchemaSPY-Documentation.md)を参照してください。 + +すでに`Database`モデルを別の用途で生成している場合は、そのモデルを使用してデータベースの接続情報を持った`Database`を構築することができます。 + +```scala 3 +import ldbc.dsl.io.* + +val database: Database = ??? + +val db = database.fromDriverManager() +// or +val db = database.fromDriverManager("user name", "password") +``` + +### メソッドチェーンでの使用 + +`Database`モデルは`TableQuery`のメソッドで`DataSource`の代わりに使用することもできます。 + +```scala 3 +val read = userQuery.selectAll.toList.readOnly(db) +val commit = userQuery.insert((1L, "name", None)).update.autoCommit(db) +val transaction = (for + result1 <- userQuery.insert((1L, "name", None)).returning("id") + result2 <- userQuery.update("name", "update name").update + ... +yield ...).transaction(db) +``` + +## HikariCPコネクションプールの使用 + +`ldbc-hikari`は、HikariCP接続プールを構築するためのHikariConfigおよびHikariDataSourceを構築するためのビルダーを提供します。 + +```scala +libraryDependencies ++= Seq( + "@ORGANIZATION@" %% "ldbc-hikari" % "@VERSION@", +) +``` + +`HikariConfigBuilder`は名前の通りHikariCPの`HikariConfig`を構築するためのビルダーです。 + +```scala 3 +val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.default.build() +``` + +`HikariConfigBuilder`には`default`と`from`メソッドがあり`default`を使用した場合、LDBC指定のパスを元にConfigから対象の値を取得して`HikariConfig`の構築を行います。 + +```text +ldbc.hikari { + jdbc_url = ... + username = ... + password = ... +} +``` + +ユーザー独自のパスを指定したい場合は`from`メソッドを使用して引数に取得したいパスを渡す必要があります。 + +```scala 3 +val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.from("custom.path").build() + +// custom.path { +// jdbc_url = ... +// username = ... +// password = ... +// } +``` + +HikariCPに設定できる内容は[公式](https://github.com/brettwooldridge/HikariCP)を参照してください。 + +Configに設定できるキーの一覧は以下になります。 + +| キー名 | 説明 | 型 | +|-------------------------------|------------------------------------------------------------------------|------------| +| `catalog` | `接続時に設定するデフォルトのカタログ名` | `String` | +| `connection_timeout` | `クライアントがプールからの接続を待機する最大ミリ秒数` | `Duration` | +| `idle_timeout` | `接続がプール内でアイドル状態であることを許可される最大時間 (ミリ秒単位)` | `Duration` | +| `leak_detection_threshold` | `接続漏れの可能性を示すメッセージがログに記録されるまでに、接続がプールから外れる時間` | `Duration` | +| `maximum_pool_size` | `アイドル接続と使用中の接続の両方を含め、プールが許容する最大サイズ` | `Int` | +| `max_lifetime` | `プール内の接続の最大寿命` | `Duration` | +| `minimum_idle` | `アイドル接続と使用中接続の両方を含め、HikariCPがプール内に維持しようとするアイドル接続の最小数` | `Int` | +| `pool_name` | `接続プールの名前` | `String` | +| `allow_pool_suspension` | `プール・サスペンドを許可するかどうか` | `Boolean` | +| `auto_commit` | `プール内の接続のデフォルトの自動コミット動作` | `Boolean` | +| `connection_init_sql` | `新しい接続が作成されたときに、その接続がプールに追加される前に実行されるSQL文字列` | `String` | +| `connection_test_query` | `接続の有効性をテストするために実行する SQL クエリ` | `String` | +| `data_source_classname` | `Connections の作成に使用する JDBC DataSourceの完全修飾クラス名` | `String` | +| `initialization_fail_timeout` | `プール初期化の失敗タイムアウト` | `Duration` | +| `isolate_internal_queries` | `内部プール・クエリ (主に有効性チェック)を、Connection.rollback()によって独自のトランザクションで分離するかどうか` | `Boolean` | +| `jdbc_url` | `JDBCのURL` | `String` | +| `readonly` | `プールに追加する接続を読み取り専用接続として設定するかどうか` | `Boolean` | +| `register_mbeans` | `HikariCPがJMXにHikariConfigMXBeanとHikariPoolMXBeanを自己登録するかどうか` | `Boolean` | +| `schema` | `接続時に設定するデフォルトのスキーマ名` | `String` | +| `username` | `DataSource.getConnection(username,password)の呼び出しに使用されるデフォルトのユーザ名` | `String` | +| `password` | `DataSource.getConnection(username,password)の呼び出しに使用するデフォルトのパスワード` | `String` | +| `driver_class_name` | `使用するDriverのクラス名` | `String` | +| `transaction_isolation` | `デフォルトのトランザクション分離レベル` | `String` | + +`HikariDataSourceBuilder`を使用することで、HikariCPの`HikariDataSource`を構築することができます。 + +接続プールはライフタイムで管理されるオブジェクトでありきれいにシャットダウンする必要があるため、ビルダーによって構築された`HikariDataSource`は`Resource`として管理されます。 + +```scala 3 +val dataSource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() +``` + +`buildDataSource`経由で構築された`HikariDataSource`は、内部でLDBC指定のパスを元にConfigから設定を取得し構築された`HikariConfig`を使用しています。 +これは`HikariConfigBuilder`の`default`経由で生成された`HikariConfig`と同等のものです。 + +もしユーザー指定の`HikariConfig`を使用したい場合は、`buildFromConfig`を使用することで`HikariDataSource`を構築することができます。 + +```scala 3 +val hikariConfig = ??? +val dataSource = HikariDataSourceBuilder.default[IO].buildFromConfig(hikariConfig) +``` + +`HikariDataSourceBuilder`を使用して構築された`HikariDataSource`は通常IOAppを使用して実行します。 + +```scala 3 +object HikariApp extends IOApp: + + val dataSourceResource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() + + def run(args: List[String]): IO[ExitCode] = + dataSourceResource.use { dataSource => + ... + } +``` + +### HikariDatabase + +HikariCPのコネクション情報を持った`Database`を構築する方法も存在します。 + +`HikariDatabase`は`HikariDataSource`と同様に`Resource`として管理されます。 +そのため通常はIOAppを使用して実行します。 + +```scala 3 +object HikariApp extends IOApp: + + val hikariConfig = ??? + val databaseResource: Resource[F, Database[F]] = HikariDatabase.fromHikariConfig[IO](hikariConfig) + + def run(args: List[String]): IO[ExitCode] = + databaseResource.use { database => + for + result <- database.readOnly(...) + yield ExitCode.Success + } +``` diff --git a/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md b/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md new file mode 100644 index 000000000..da33f7789 --- /dev/null +++ b/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md @@ -0,0 +1,39 @@ +{% +laika.title = プレーンなSQLクエリ +laika.metadata.language = ja +%} + +# プレーンなSQLクエリ + +時には、抽象度の高いレベルではうまくサポートされていない操作のために、独自のSQLコードを書く必要があるかもしれません。JDBCの低レイヤーに戻る代わりに、ScalaベースのAPIでLDBCのPlain SQLクエリーを使うことができます。 +この章では、そのような場合にLDBCでPlain SQLクエリーを使用してデータベースへの接続処理を行うための方法について説明します。 + +プロジェクトへの依存関係やDataSourceの使用とログに関しては、前章の[データベース接続](/ja/04-Database-Connection.md#query)の章を参照してください。 + +## Plain SQL + +LDBCでは以下のようにsql文字列補間をリテラルSQL文字列で使用してプレーンなクエリを構築します。 + +クエリに注入された変数や式は、結果のクエリ文字列のバインド変数に変換されます。クエリ文字列に直接挿入されるわけではないので、SQLインジェクション攻撃の危険はありません。 + +```scala 3 +val select = sql"SELECT id, name, age FROM user WHERE id = $id" // SELECT id, name, age FROM user WHERE id = ? +val insert = sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)" // INSERT INTO user (id, name, age) VALUES(?, ?, ?) +val update = sql"UPDATE user SET id = $id, name = $name, age = $age" // UPDATE user SET id = ?, name = ?, age = ? +val delete = sql"DELETE FROM user WHERE id = $id" // DELETE FROM user WHERE id = ? +``` + +Plain SQLクエリーは実行時にSQL文を構築するだけです。これは安全かつ簡単に複雑なステートメントを構築する方法を提供しますが、これは単なる埋め込み文字列にすぎません。ステートメントに構文エラーがあったり、データベースとScalaコードの型が一致しなかったりしてもコンパイル時に検出することはできません。 + +クエリ実行結果の戻り値の型、接続方法の設定に関しては前章の「データベース接続」にある[Query](/ja/04-Database-Connection.md)項目以降を参照してください。 +テーブル定義を使用して構築されたクエリと同じように構築および動作します。 + +プレーンなクエリと型安全なクエリは構築方法が違うだけで後続の接続方法などは同じ実装です。そのため2つを組み合わせてクエリを実行することも可能です。 + +```scala 3 +(for + result1 <- sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)".update + result2 <- userQuery.update("name", "update name").update + ... +yield ...).transaction +``` diff --git a/docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md b/docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md new file mode 100644 index 000000000..2111b90fe --- /dev/null +++ b/docs/src/main/mdoc/ja/06-Generating-SchemaSPY-Documentation.md @@ -0,0 +1,79 @@ +{% +laika.title = SchemaSPYドキュメントの生成 +laika.metadata.language = ja +%} + +# SchemaSPYドキュメントの生成 + +この章では、LDBCで構築したテーブル定義を使用して、SchemaSPYドキュメントの作成を行うための方法について説明します。 + +プロジェクトに以下の依存関係を設定する必要があります。 + +```scala +libraryDependencies += "@ORGANIZATION@" %% "ldbc-schemaspy" % "@VERSION@" +``` + +LDBCでのテーブル定義方法をまだ読んでいない場合は、[テーブル定義](/ja/01-Table-Definitions.md)の章を先に読むことをオススメしましす。 + +以下のコード例では、以下のimportを想定しています。 + +```scala 3 +import ldbc.core.* +import ldbc.schemaspy.SchemaSpyGenerator +``` + +## テーブル定義から生成 + +SchemaSPYはデータベースへ接続を行いMeta情報やテーブル構造を取得しその情報を元にドキュメントを生成しますが、LDBCではデータベースへの接続は行わずLDBCで構築したテーブル構造を使用してSchemaSPYのドキュメントを生成します。 +データベースへの接続を行わないためシンプルにSchemaSPYを使用して生成したドキュメントと乖離する項目があります。例えば、現在テーブルに保存されているレコード数などの情報は表示することができません。 + +ドキュメントを生成するためにはデータベースの情報が必要です。LDBCではデータベースの情報を表現するためのtraitが存在しています。 + +`ldbc.core.Database`を使用してデータベース情報を構築したサンプルは以下になります。 + +```scala 3 +case class SampleLdbcDatabase( + schemaMeta: Option[String] = None, + catalog: Option[String] = Some("def"), + host: String = "127.0.0.1", + port: Int = 3306 +) extends Database: + + override val databaseType: Database.Type = Database.Type.MySQL + + override val name: String = "sample_ldbc" + + override val schema: String = "sample_ldbc" + + override val character: Option[Character] = None + + override val collate: Option[Collate] = None + + override val tables = Set( + ... // LDBCで構築したテーブル構造を列挙 + ) +``` + +SchemaSPYのドキュメント生成には`SchemaSpyGenerator`を使用します。生成したデータベース定義を`default`メソッドに渡し、`generate`を呼び出すと第2引数に指定したファイルの場所にSchemaSPYのファイル群が生成されます。 + +```scala 3 +@main +def run(): Unit = + val file = java.io.File("document") + SchemaSpyGenerator.default(SampleLdbcDatabase(), file).generate() +``` + +生成されたファイルの`index.html`を開くとSchemaSPYのドキュメントを確認することができます。 + +## データベース接続から生成 + +SchemaSpyGeneratorには`connect`メソッドも存在しています。こちらは標準のSchemaSpyの生成方法と同様にデータベースに接続を行いドキュメントの生成を行います。 + +```scala 3 +@main +def run(): Unit = + val file = java.io.File("document") + SchemaSpyGenerator.connect(SampleLdbcDatabase(), "user name", "password" file).generate() +``` + +データベース接続を行う処理はSchemaSpy内部のJavaで書かれた実装で行われます。そのためEffectシステムでスレッドなどが管理されていないことに注意してください。 diff --git a/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md b/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md new file mode 100644 index 000000000..f816b6f49 --- /dev/null +++ b/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md @@ -0,0 +1,205 @@ +{% +laika.title = スキーマコード生成 +laika.metadata.language = ja +%} + +# スキーマコード生成 + +この章では、LDBCのテーブル定義をSQLファイルから自動生成する方法について説明します。 + +プロジェクトに以下の依存関係を設定する必要があります。 + +```scala 3 +addSbtPlugin("@ORGANIZATION@" % "ldbc-plugin" % "@VERSION@") +``` + +## 生成 + +プロジェクトに対してプラグインを有効にします。 + +```sbt +lazy val root = (project in file(".")) + .enablePlugins(Ldbc) +``` + +解析対象のSQLファイルを配列で指定します。 + +```sbt +Compile / parseFiles := List(baseDirectory.value / "test.sql") +``` + +**プラグインを有効にすることで設定できるキーの一覧** + +| キー | 詳細 | +|--------------------|------------------------------------------| +| parseFiles | 解析対象のSQLファイルのリスト | +| parseDirectories | 解析対象のSQLファイルをディレクトリ単位で指定する | +| excludeFiles | 解析から除外するファイル名のリスト | +| customYamlFiles | Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト。 | +| classNameFormat | クラス名の書式を指定する値。 | +| propertyNameFormat | Scalaモデルのプロパティ名の形式を指定する値。 | +| ldbcPackage | 生成されるファイルのパッケージ名を指定する値。 | + +解析対象のSQLファイルの先頭には必ずデータベースのCreate文もしくはUse文を定義する必要があります。LDBCはファイルの解析を1ファイルずつ行い、テーブル定義を生成しデータベースモデルにテーブルのリストを格納させます。 +そのためテーブルがどのデータベースに所属しているかを教えてあげる必要があるからです。 + +```sql +CREATE DATABASE `location`; + +USE `location`; + +DROP TABLE IF EXISTS `country`; +CREATE TABLE country ( + `id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `code` INT NOT NULL +); +``` + +解析対象のSQLファイルにはデータベースのCreate/Use文もしくはテーブル定義のCreate/Drop文のみ記載するようにしなければいけません。 + +## 生成コード + +sbtプロジェクトを起動してコンパイルを実行すると、解析対象のSQLファイルを元に生成されたモデルクラスと、テーブル定義がsbtプロジェクトのtarget配下に生成されます。 + +```shell +sbt compile +``` + +上記SQLファイルから生成されるコードは以下のようなものになります。 + +```scala 3 +package ldbc.generated.location + +import ldbc.core.* + +case class Country( + id: Long, + name: String, + code: Int +) + +object Country: + val table = Table[Country]("country")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("code", INT) + ) +``` + +Compileでコードを生成した場合、その生成されたファイルはキャッシュされるので、SQLファイルを変更していない場合再度生成されることはありません。SQLファイルを変更した場合もしくは、cleanコマンドを実行してキャッシュを削除した場合はCompileを実行すると再度コードが生成されます。 +キャッシュを利用せず再度コード生成を行いたい場合は、`generateBySchema`コマンドを実行してください。このコマンドはキャッシュを使用せず常にコード生成を行います。 + +```shell +sbt generateBySchema +``` + +## カスタマイズ + +SQLファイルから生成されるコードの型を別のものに変換したい時があるかもしれません。その場合は`customYamlFiles`にカスタマイズを行うymlファイルを渡してあげることで行うことができます。 + +```sbt +Compile / customYamlFiles := List( + baseDirectory.value / "custom.yml" +) +``` + +ymlファイルの形式は以下のようなものである必要があります。 + +```yaml +database: + name: '{データベース名}' + tables: + - name: '{テーブル名}' + columns: # Optional + - name: '{カラム名}' + type: '{変更したいScalaの型}' + class: # Optional + extends: + - '{モデルクラスに継承させたいtraitなどのpackageパス}' // package.trait.name + object: # Optional + extends: + - '{オブジェクトに継承させたいtraitなどのpackageパス}' + - name: '{テーブル名}' + ... +``` + +`database`は解析対象のSQLファイルに記載されているデータベース名である必要があります。またテーブル名は解析対象のSQLファイルに記載されているデータベースに所属しているテーブル名である必要があります。 + +`columns`には型を変更したいカラム名と変更したいScalaの型を文字列で記載を行います。`columns`には複数の値を設定できますが、nameに記載されたカラム名が対象のテーブルに含まれいてなければなりません。 +また、変換を行うScalaの型はカラムのData型がサポートしている型である必要があります。もしサポート対象外の型を指定したい場合は、`object`に対して暗黙の型変換を行う設定を持ったtraitやabstract classなどを渡してあげる必要があります。 + +Data型がサポートしている型に関しては[こちら](/ja/01-Table-Definitions.md)を、サポート対象外の型を設定する方法は[こちら](/ja/02-Custom-Data-Type.md)を参照してください。 + +Int型をユーザー独自の型であるCountryCodeに変換する場合は、以下のような`CustomMapping`traitを実装します。 + +```scala 3 +trait CountryCode: + val code: Int +object Japan extends CountryCode: + override val code: Int = 1 + +trait CustomMapping: // 任意の名前 + given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode] +``` + +カスタマイズを行うためのymlファイルに実装を行なった`CustomMapping`traitを設定し、対象のカラムの型をCountryCodeに変換してあげます。 + +```yaml +database: + name: 'location' + tables: + - name: 'country' + columns: + - name: 'code' + type: 'Country.CountryCode' // CustomMappingをCountryオブジェクトにミックスインさせるのでそこから取得できるように記載 + object: + extends: + - '{package.name.}CustomMapping' +``` + +上記設定で生成されるコードは以下のようになり、ユーザー独自の型でモデルとテーブル定義を生成できるようになります。 + +```scala 3 +case class Country( + id: Long, + name: String, + code: Country.CountryCode +) + +object Country extends /*{package.name.}*/CustomMapping: + val table = Table[Country]("country")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("code", INT) + ) +``` + +データベースモデルに関してもSQLファイルから自動生成が行われています。 + +```scala 3 +package ldbc.generated.location + +import ldbc.core.* + +case class LocationDatabase( + schemaMeta: Option[String] = None, + catalog: Option[String] = Some("def"), + host: String = "127.0.0.1", + port: Int = 3306 +) extends Database: + + override val databaseType: Database.Type = Database.Type.MySQL + + override val name: String = "location" + + override val schema: String = "location" + + override val character: Option[Character] = None + + override val collate: Option[Collate] = None + + override val tables = Set( + Country.table + ) +``` diff --git a/docs/src/main/mdoc/ja/08-Performance.md b/docs/src/main/mdoc/ja/08-Performance.md new file mode 100644 index 000000000..543c1e3be --- /dev/null +++ b/docs/src/main/mdoc/ja/08-Performance.md @@ -0,0 +1,38 @@ +{% +laika.title = パフォーマンス +laika.metadata.language = ja +%} + +# パフォーマンス + +## コンパイル時間のオーバーヘッド + +テーブル定義のコンパイル時間はカラムの数に応じて増加する + +@:image(../img/compile_create.png) {} + +クエリ構築のコンパイル時間はselectするカラム数に応じて増加する + +@:image(../img/compile_create_query.png) {} + +## ランタイムのオーバーヘッド + +ldbcは内部的にはTupleを使用しているので、純粋なクラス定義に比べてかなり遅くなってしまう。 + +@:image(../img/runtime_create.png) {} + +ldbcはテーブル定義で他に比べてかなり遅くなってしまう。 + +@:image(../img/runtime_create_query.png) {} + +## クエリ実行のオーバーヘッド + +selectクエリの実行は取得するレコード数が増加するにつれてスループットは低くなる + +@:image(../img/select_throughput.png) {} + +insertクエリの実行は挿入するレコード数が増加するにつれてスループットは低くなる + +※ 実行したクエリが完全に一致するものではないため正確ではない + +@:image(../img/insert_throughput.png) {} diff --git a/docs/src/main/mdoc/ja/directory.conf b/docs/src/main/mdoc/ja/directory.conf index 2c56f2aa1..c54ff294b 100644 --- a/docs/src/main/mdoc/ja/directory.conf +++ b/docs/src/main/mdoc/ja/directory.conf @@ -1,6 +1,12 @@ laika.navigationOrder = [ index.md - tutorial - reference + 01-Table-Definitions.md + 02-Custom-Data-Type.md + 03-Type-safe-Query-Builder.md + 04-Database-Connection.md + 05-Plain-SQL-Queries.md + 06-Generating-SchemaSPY-Documentation.md + 07-Schema-Code-Generation.md + 08-Performance.md ] laika.versioned = true diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index 0251f4191..a98de095a 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -3,47 +3,87 @@ laika.title = ldbc laika.metadata.language = ja %} -# ldbc (Lepus Database Connectivity) +# LDBC -**ldbc**は1.0以前のソフトウェアであり、現在も活発に開発中であることに注意してください。新しいバージョンは以前のバージョンとバイナリ互換性がなくなってしまう可能性があります。 - -ldbcは、[Cats Effect 3](https://typelevel.org/cats-effect/)と[Scala 3](https://github.com/scala/scala3)による純粋関数型JDBCレイヤーを構築するためのライブラリです。 - -ldbcは[Typelevel](http://typelevel.org/)プロジェクトです。これは、Scalaの[行動規範](http://scala-lang.org/conduct.html)に記載されているように、純粋で、型にはまらない、関数型プログラミングを受け入れ、教育、学習、貢献のための安全でフレンドリーな環境を提供することを意味します。 +**LDBC**は1.0以前のソフトウェアであり、現在も活発に開発中であることに注意してください。新しいバージョンは以前のバージョンとバイナリ互換性がなくなってしまう可能性があります。 ## はじめに -私たちのアプリケーション開発では大抵の場合データベースを使用します。 - -Scalaでデータベースアクセスを行う場合JDBCを使用する方法がありますが、ScalaにはこのJDBCをラップしたライブラリがいくつか存在しています。 +私たちのアプリケーション開発では大抵の場合データベースを使用します。
Scalaでデータベースアクセスを行う場合JDBCを使用する方法がありますが、ScalaにはこのJDBCをラップしたライブラリがいくつか存在しています。 - 関数型DSL (Slick, quill, zio-sql) - SQL文字列インターポレーター (Anorm, doobie) -ldbcも同じくJDBCをラップしたライブラリであり、ldbcはそれぞれの側面を組み合わせたScala 3ライブラリで型安全でリファクタブルなSQLインターフェイスを提供し、MySQLのデータベース上でのSQL式を表現できます。 +LDBCも同じくJDBCをラップしたライブラリであり、LDBCはそれぞれの側面を組み合わせたScala 3ライブラリで、型安全でリファクタブルなSQLインターフェイスを提供し、MySQLのデータベース上でのSQL式を表現できます。 + +また、LDBCのコンセプトは、LDBCを使用することで単一リソースを管理することでScalaのモデルやsqlのスキーマ、ドキュメントを一元化できる開発を行えることです。 + +このコンセプトは宣言的でタイプセーフなWebエンドポイントライブラリである[tapir](https://github.com/softwaremill/tapir)から影響を受けました。
tapirを使用することで、型安全なエンドポイントを構築することができ、構築したエンドポイントからOpenAPIドキュメントを生成することもできます。 + +LDBCはデータベース層でScalaを使用して、同じように型安全な構築を可能にし、構築されたものを使用してドキュメントの生成を行えるようにします。 -ldbcは他のライブラリとは異なり、Scalaで構築された独自のコネクターも提供しています。 +## なぜLDBCなのか? -Scalaは現在JVM, JS, Nativeというマルチプラットフォームに対応しています。 +データベースを利用したアプリケーション開発では、様々な変更を継続的に行う必要があります。 -しかし、JDBCを使用したライブラリだとJVM環境でしか動作しません。 +例えば、データベースに構築されたテーブルのどの情報をアプリケーションで扱うべきか、データ検索にはどのようなクエリが最適か、などである。 -そのためldbcは、MySQLプロトコルに対応したScalaで書かれたコネクタを提供することで異なるプラットフォームで動作できるようにするために開発を行っています。 -ldbcを使用することで、Scalaの型安全性と関数型プログラミングの利点を活かしながら、プラットフォームを問わずにデータベースアクセスを行うことができます。 +テーブル定義にカラムを1つ追加するだけでも、SQLファイルの修正、対応するモデルへのプロパティの追加、データベースへの反映、ドキュメントの更新などが必要になります。 -また、ldbcを使用することで単一リソースを管理することでScalaのモデルやsqlのスキーマ、ドキュメントを一元化できる開発を行えることです。 +他にも考慮すべきこと、修正すべきことなどたくさんあります。 -このコンセプトは宣言的でタイプセーフなWebエンドポイントライブラリである[tapir](https://github.com/softwaremill/tapir)から影響を受けました。tapirを使用することで型安全なエンドポイントを構築することができ、構築したエンドポイントからOpenAPIドキュメントを生成することもできます。 +日々の開発の中で全てをメンテナンスし続けるのはとても大変なことであり、メンテナンス漏れだって起こるかもしれません。 -ldbcはデータベース層でScalaを使用して、同じように型安全な構築を可能にし構築されたものを使用してドキュメントの生成を行えるようにします。 +テーブル情報をアプリケーション・モデルにマッピングすることなく、プレーンなSQLでデータを取得し、データを取得する際には指定された型で取得するというアプローチは非常に良い方法だと思います。 -### 対象読者 +この方法であれば、データベース固有のモデルを構築する必要がなく、開発者はデータを取得したいときに、取得したい種類のデータを使って自由にデータを扱うことができるからです。
また、プレーンなクエリを扱うことで、どのようなクエリが実行されるかを瞬時に把握できる点も非常に優れていると思います。 -このドキュメントは、Scalaプログラミング言語を使用してデータベースアクセスを行うためのライブラリであるldbcを使用する開発者を対象としています。 +しかし、この方法ではテーブル情報のアプケーションでの管理がなくなっただけでドキュメントの更新などを解消することはできません。 -ldbcは、型付けされた純粋な関数型プログラミングに興味がある人のために設計されています。もしあなたがCatsユーザーでなかったり、関数型I/OやモナドCats Effectに馴染みがなかったりする場合は、ゆっくり進める必要があるかもしれません。 +LDBCは、これらの問題のいくつかを解決するために開発されています。 -とはいえ、もしこのドキュメントやldbc APIに戸惑ったり苛立ったりしたら、issueを発行して助けを求めてください。ライブラリもドキュメントも歴史が浅く、急速に変化しているため、不明瞭な点があるのは避けられません。従って、本書は問題や脱落に対処するために継続的に更新されます。 +- 型安全性:コンパイル時の保証、開発時の補完、読み取り時の情報 +- 宣言型:テーブル定義の形("What")とデータベース接続("How")を分離する。 +- SchemaSPYの統合:テーブル記述からドキュメントを生成する +- フレームワークではなくライブラリ: あなたのスタックに統合できる + +LDBCを使用するとデータベースの情報をアプリケーションで管理しなければいけませんが、型安全性とクエリの構築、ドキュメントの管理を一元化することができます。 + +LDBCでのモデルをテーブル定義にマッピングするのはとても簡単です。 + +モデルが持つプロパティと、そのカラムのために定義されるデータ型の間のマッピングも非常にシンプルです。開発者は、モデルが持つプロパティと同じ順序で、対応するカラムを定義するだけです。 + +```scala 3 +import ldbc.core.* + +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), +) +``` + +また、間違った型を組み合わせようとするとコンパイルエラーになります。 + +例えば、Userが持つString型のnameプロパティに関連するカラムにINT型のカラムを渡すとエラーになります。 + +```shell +[error] -- [E007] Type Mismatch Error: +[error] 169 | column("name", INT), +[error] | ^^^ +[error] |Found: ldbc.core.DataType.Integer[T] +[error] |Required: ldbc.core.DataType[String] +[error] | +[error] |where: T is a type variable with constraint <: Int | Long | Option[Int | Long] +``` + +これらのアドオンの詳細については、[テーブル定義](/ja/01-Table-Definitions.md) を参照してください。 ## クイックスタート @@ -53,18 +93,17 @@ ldbcは、型付けされた純粋な関数型プログラミングに興味が libraryDependencies ++= Seq( // まずはこの1つから - "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", - - // 使用するコネクタを選択 - "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@", // Javaコネクタ (対応プラットフォーム: JVM) - "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@", // Scalaコネクタ (対応プラットフォーム: JVM, JS, Native) + "@ORGANIZATION@" %% "ldbc-core" % "@VERSION@", // そして、必要に応じてこれらを加える + "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", // プレーンクエリー データベース接続 "@ORGANIZATION@" %% "ldbc-query-builder" % "@VERSION@", // 型安全なクエリ構築 - "@ORGANIZATION@" %% "ldbc-schema" % "@VERSION@", // データベーススキーマの構築 + "@ORGANIZATION@" %% "ldbc-schemaspy" % "@VERSION@", // SchemaSPYドキュメント生成 ) ``` +sbtプラグインの使い方については、こちらの[documentation](/ja/07-Schema-Code-Generation.md)を参照してください。 + ## TODO - JSONデータタイプのサポート @@ -74,5 +113,6 @@ libraryDependencies ++= Seq( - MySQL以外のデータベースサポート - ストリーミングのサポート - ZIOモジュールのサポート +- 他データベースライブラリとの統合 - テストキット - etc... From 899272886525b9f533575eaba081436b6496d248 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:18:55 +0900 Subject: [PATCH 150/160] Delete unused --- docs/src/main/mdoc/en/reference/Connector.md | 842 ------------------ .../src/main/mdoc/en/reference/directory.conf | 5 - docs/src/main/mdoc/en/reference/index.md | 14 - docs/src/main/mdoc/en/tutorial/Connection.md | 93 -- .../main/mdoc/en/tutorial/Custom-Data-Type.md | 67 -- .../mdoc/en/tutorial/Database-Operations.md | 53 -- .../main/mdoc/en/tutorial/Error-Handling.md | 37 - docs/src/main/mdoc/en/tutorial/Logging.md | 52 -- .../mdoc/en/tutorial/Parameterized-Queries.md | 112 --- .../main/mdoc/en/tutorial/Query-Builder.md | 392 -------- .../en/tutorial/Schema-Code-Generation.md | 205 ----- docs/src/main/mdoc/en/tutorial/Schema.md | 485 ---------- .../main/mdoc/en/tutorial/Selecting-Data.md | 147 --- docs/src/main/mdoc/en/tutorial/Setup.md | 183 ---- .../main/mdoc/en/tutorial/Simple-Program.md | 106 --- .../main/mdoc/en/tutorial/Updating-Data.md | 115 --- docs/src/main/mdoc/en/tutorial/directory.conf | 17 - docs/src/main/mdoc/en/tutorial/index.md | 15 - 18 files changed, 2940 deletions(-) delete mode 100644 docs/src/main/mdoc/en/reference/Connector.md delete mode 100644 docs/src/main/mdoc/en/reference/directory.conf delete mode 100644 docs/src/main/mdoc/en/reference/index.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Connection.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Database-Operations.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Error-Handling.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Logging.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Query-Builder.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Schema.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Selecting-Data.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Setup.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Simple-Program.md delete mode 100644 docs/src/main/mdoc/en/tutorial/Updating-Data.md delete mode 100644 docs/src/main/mdoc/en/tutorial/directory.conf delete mode 100644 docs/src/main/mdoc/en/tutorial/index.md diff --git a/docs/src/main/mdoc/en/reference/Connector.md b/docs/src/main/mdoc/en/reference/Connector.md deleted file mode 100644 index 5eaf4c8bc..000000000 --- a/docs/src/main/mdoc/en/reference/Connector.md +++ /dev/null @@ -1,842 +0,0 @@ -{% - laika.title = Connector - laika.metadata.language = en -%} - -# Connector - -This chapter describes database connections using ldbc's own MySQL connector. - -To connect to a MySQL database in Scala, you need to use JDBC, which is a standard Java API that can also be used in Scala. -Since JDBC is implemented in Java, it can only be used in the JVM environment when used in Scala. - -The recent environment surrounding Scala has seen a lot of plugin development to enable Scala to run in JS, Native, and other environments. -Scala has evolved from a JVM-only language that can use Java assets to a language that can run on multiple platforms. - -However, JDBC is a standard Java API that does not support Scala's multi-platform behavior. - -Therefore, even if you create an application in Scala to work with JS, Native, etc., you will not be able to connect to MySQL or other databases because you cannot use JDBC. - -The Typelevel Project has a Scala library for [PostgreSQL](https://www.postgresql.org/) called [Skunk](https://github.com/typelevel/skunk). -This project does not use JDBC and uses pure Scala only to connect to PostgreSQL. Therefore, Skunk can be used to connect to PostgreSQL in any JVM, JS, or Native environment. - -The ldbc connector is a Skunk-inspired project that is being developed to enable connections to MySQL in any JVM, JS, or Native environment. - -※ This connector is currently an experimental feature. Therefore, it should not be used in a production environment. - -The ldbc connector is the lowest layer API. -We plan to use this connector to provide higher-layer APIs in the future. We also plan to make it compatible with existing higher-layer APIs. - -To use this connector, the following dependencies must be set up in your project. - -**JVM** - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" -``` - -**JS/Native** - -```scala 3 -libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" -``` - -**Supported Versions** - -The current version supports the following versions of MySQL - -- MySQL 5.7.x -- MySQL 8.x - -The main support is for MySQL 8.x. MySQL 5.7.x is a sub-support. Therefore, be careful when working with MySQL 5.7.x. -We plan to discontinue support for MySQL 5.7.x in the future. - -## Connection - -Connection` is used to connect to MySQL using the ldbc connector. - -In addition, `Connection` can use `Otel4s` to collect telemetry data so that it can be developed with obserbability in mind. -Therefore, when using `Connection`, the `Tracer` of `Otel4s` must be configured. - -It is recommended to use `Tracer.noop` during development or if you do not need telemetry data using tracing. - -```scala 3 -import cats.effect.IO -import org.typelevel.otel4s.trace.Tracer -import ldbc.connector.Connection - -given Tracer[IO] = Tracer.noop[IO] - -val connection = Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "root", -) -``` - -The following is a list of properties that can be set when constructing a `Connection - -| Property | Type | Use | -|---------------------------|----------------------|--------------------------------------------------------------------------------------------------------------| -| `host` | `String` | `Specify the host for the MySQL server` | -| `port` | `Int` | `Specify the port number of the MySQL server` | -| `user` | `String` | `Specify the user name to log in to the MySQL server.` | -| `password` | `Option[String]` | `Specify the password of the user who will log in to the MySQL server.` | -| `database` | `Option[String]` | `Specify the database name to be used after connecting to the MySQL server` | -| `debug` | `Boolean` | `Outputs a log of the process. Default is false.` | -| `ssl` | `SSL` | `Specifies whether SSL/TLS is used for notifications to and from the MySQL server. The default is SSL.None.` | -| `socketOptions` | `List[SocketOption]` | `Specifies socket options for TCP/UDP sockets.` | -| `readTimeout` | `Duration` | `Specifies the timeout before an attempt is made to connect to the MySQL server. Default is Duration.Inf.` | -| `allowPublicKeyRetrieval` | `Boolean` | `Specifies whether to use the RSA public key when authenticating with the MySQL server. Default is false.` | - -Connection` uses `Resource` to manage resources. Therefore, when connection information is used, the `use` method is used to manage the resource. - -```scala 3 -connection.use { conn => - // コードを記述 -} -``` - -### Authentication - -Authentication in MySQL involves the client sending user information in a phase called LoginRequest when connecting to the MySQL server. The server then looks up the user in the `mysql.user` table to determine which authentication plugin to use. After the authentication plugin is determined, the server calls the plugin to initiate user authentication and sends the results to the client side. In this way, authentication is pluggable in MySQL. - -The authentication plug-ins supported by MySQL are listed on the [official page](https://dev.mysql.com/doc/refman/8.0/ja/authentication-plugins.html). - -ldbc currently supports the following authentication plug-ins - -- Native Pluggable Authentication -- SHA-256 Pluggable Authentication -- SHA-2 Cached Pluggable Authentication - -※ Native pluggable authentication and SHA-256 pluggable authentication are plugins that have been deprecated since MySQL 8.x. It is recommended that you use the SHA-2 pluggable authentication cache unless you have a good reason to do otherwise. - -There is no need to be aware of authentication plug-ins in the ldbc application code. Users simply create a user created with the authentication plugin they wish to use on the MySQL database and then attempt to connect to MySQL using that user in the ldbc application code. -ldbc will internally determine the authentication plugin and use the appropriate authentication plugin to connect to MySQL. - -## Execution - -The following tables are assumed to be used in the subsequent process. - -```sql -CREATE TABLE users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - age INT NULL -); -``` - -### Statement - -`Statement` is an API for executing SQL without dynamic parameters. - -※ Since `Statement` does not use dynamic parameters, there is a risk of SQL injection depending on its usage. Therefore, it is recommended to use `PreparedStatement` when dynamic parameters are used. - -Construct a `Statement` using the `createStatement` method of `Connection`. - -#### Read Query - -Use the `executeQuery` method to execute read-only SQL. - -The value returned by the MySQL server as a result of executing the query is stored in a `ResultSet` and returned as a return value. - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeQuery("SELECT * FROM users") - yield - // Processing with ResultSet -} -``` - -#### Write Query - -Use the `executeUpdate` method to execute the SQL to be written. - -The value returned by the MySQL server as a result of executing the query is the number of rows affected. - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - result <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)") - yield -} -``` - -#### Get the value of AUTO_INCREMENT - -If you want to get the value of AUTO_INCREMENT after executing a query using `Statement`, use the method `getGeneratedKeys`. - -The value returned by the MySQL server as a result of executing the query will be the value generated for AUTO_INCREMENT as the return value. - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 20)", Statement.RETURN_GENERATED_KEYS) - gereatedKeys <- statement.getGeneratedKeys() - yield -} -``` - -### Client/Server PreparedStatement - -ldbc provides `PreparedStatement` divided into `Client PreparedStatement` and `Server PreparedStatement`. - -The `Client PreparedStatement` is an API for constructing SQL on the application using dynamic parameters and sending it to the MySQL server. -Therefore, the method of sending queries to the MySQL server is the same as for `Statement`. - -This API is equivalent to JDBC's `PreparedStatement`. - -Use the `Server PreparedStatement` for building queries in the MySQL server, which is more secure. - -The `Server PreparedStatement` allows queries to be reused since the query to be executed and parameters are sent separately. - -When using `Server PreparedStatement`, the query is prepared in advance by the MySQL server. Although the MySQL server uses memory to store them, the queries can be reused, which improves performance. - -However, there is a risk of memory leaks because the pre-prepared queries will continue to use memory until they are freed. - -If you use `Server PreparedStatement`, you must use the `close` method to properly release the query. - -#### Client PreparedStatement - -Construct a `Client PreparedStatement` using the `ClientPreparedStatement` method of `Connection`. - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### Server PreparedStatement - -Construct a `Server PreparedStatement` using the `Connection` `serverPreparedStatement` method. - -```scala 3 -connection.use { conn => - for - statement <- conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - ... - yield ... -} -``` - -#### Read Query - -Use the `executeQuery` method to execute read-only SQL. - -The value returned by the MySQL server as a result of executing the query is stored in a `ResultSet` and returned as a return value. - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - yield - // Processing with ResultSet -} -``` - -If you want to use dynamic parameters, use the `setXXX` method to set the parameters. -The `setXXX` method can also use the `Option` type. If `None` is passed, the parameter will be set to NULL. - -The `setXXX` method specifies the index of the parameter and the value of the parameter. - -```scala 3 -statement.setLong(1, 1) -``` - -The following methods are supported in the current version - -| Method | Type | Note | -|-----------------|---------------------------------------|----------------------------------------------------| -| `setNull` | | Set the parameter to NULL | -| `setBoolean` | `Boolean/Option[Boolean]` | | -| `setByte` | `Byte/Option[Byte]` | | -| `setShort` | `Short/Option[Short]` | | -| `setInt` | `Int/Option[Int]` | | -| `setLong` | `Long/Option[Long]` | | -| `setBigInt` | `BigInt/Option[BigInt]` | | -| `setFloat` | `Float/Option[Float]` | | -| `setDouble` | `Double/Option[Double]` | | -| `setBigDecimal` | `BigDecimal/Option[BigDecimal]` | | -| `setString` | `String/Option[String]` | | -| `setBytes` | `Array[Byte]/Option[Array[Byte]]` | | -| `setDate` | `LocalDate/Option[LocalDate]` | Directly handle `java.time` instead of `java.sql`. | -| `setTime` | `LocalTime/Option[LocalTime]` | Directly handle `java.time` instead of `java.sql`. | -| `setTimestamp` | `LocalDateTime/Option[LocalDateTime]` | Directly handle `java.time` instead of `java.sql`. | -| `setYear` | `Year/Option[Year]` | Directly handle `java.time` instead of `java.sql`. | - -#### Write Query - -Use the `executeUpdate` method to execute the SQL to be written. - -The value returned by the MySQL server as a result of executing the query is the number of rows affected. - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - yield result -} - -``` - -#### Get the value of AUTO_INCREMENT - -To get the value of AUTO_INCREMENT after executing a query, use the `getGeneratedKeys` method. - -The value returned by the MySQL server as a result of executing the query will be the value generated for AUTO_INCREMENT as the return value. - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.executeUpdate() - getGeneratedKeys <- statement.getGeneratedKeys() - yield getGeneratedKeys -} -``` - -### ResultSet - -The `ResultSet` is an API for storing values returned by the MySQL server after query execution. - -There are two ways to retrieve records retrieved by executing SQL from `ResultSet`: using the `next` and `getXXX` methods as in JDBC, or using ldbc's own `decode` method. - -#### next/getXXX - -The `next` method returns `true` if the next record exists, or `false` if the next record does not exist. - -The `getXXX` method is an API for retrieving values from records. - -The `getXXX` method can be used either by specifying the index of the column to be retrieved or by specifying the column name. - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT `id`, `name`, `age` FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - records <- Monad[IO].whileM(result.next()) { - for - id <- result.getLong(1) - name <- result.getString("name") - age <- result.getInt(3) - yield (id, name, age) - } - yield records -} -``` - -#### decode - -The `decode` method is an API for converting values retrieved from a `ResultSet` to a Scala type. - -The type to be converted is specified using the `*:` operator depending on the number of columns to be retrieved. - -The example shows how to retrieve the id, name, and age columns of the users table, specifying the type of each column. - -```scala 3 -result.decode(bigint *: varchar *: int.opt) -``` - -If you want to get a NULL-allowed column, use the `opt` method to convert it to the `Option` type. -If the record is NULL, it can be retrieved as None. - -The sequence of events from query execution to record retrieval is as follows - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") - _ <- statement.setLong(1, 1) - result <- statement.executeQuery() - decodes <- result.decode(bigint *: varchar *: int.opt) - yield decodes -} -``` - -The records retrieved from a `ResultSet` will always be an array. -This is because a query in MySQL may always return multiple records. - -If you want to retrieve a single record, use the `head` or `headOption` method after the `decode` process. - -The following data types are supported in the current version - -| Codec | Data Type | Scala Type | -|---------------|---------------------|------------------| -| `boolean` | `BOOLEAN` | `Boolean` | -| `tinyint` | `TINYINT` | `Byte` | -| `utinyint` | `unsigned TINYINT` | `Short` | -| `smallint` | `SMALLINT` | `Short` | -| `usmallint` | `unsigned SMALLINT` | `Int` | -| `int` | `INT` | `Int` | -| `uint` | `unsigned INT` | `Long` | -| `bigint` | `BIGINT` | `Long` | -| `ubigint` | `unsigned BIGINT` | `BigInt` | -| `float` | `FLOAT` | `Float` | -| `double` | `DOUBLE` | `Double` | -| `decimal` | `DECIMAL` | `BigDecimal` | -| `char` | `CHAR` | `String` | -| `varchar` | `VARCHAR` | `String` | -| `binary` | `BINARY` | `Array[Byte]` | -| `varbinary` | `VARBINARY` | `String` | -| `tinyblob` | `TINYBLOB` | `String` | -| `blob` | `BLOB` | `String` | -| `mediumblob` | `MEDIUMBLOB` | `String` | -| `longblob` | `LONGBLOB` | `String` | -| `tinytext` | `TINYTEXT` | `String` | -| `text` | `TEXT` | `String` | -| `mediumtext` | `MEDIUMTEXT` | `String` | -| `longtext` | `LONGTEXT` | `String` | -| `enum` | `ENUM` | `String` | -| `set` | `SET` | `List[String]` | -| `json` | `JSON` | `String` | -| `date` | `DATE` | `LocalDate` | -| `time` | `TIME` | `LocalTime` | -| `timetz` | `TIME` | `OffsetTime` | -| `datetime` | `DATETIME` | `LocalDateTime` | -| `timestamp` | `TIMESTAMP` | `LocalDateTime` | -| `timestamptz` | `TIMESTAMP` | `OffsetDateTime` | -| `year` | `YEAR` | `Year` | - -※ Currently, it is designed to retrieve values by specifying the MySQL data type, but in the future it may be changed to a more concise Scala type to retrieve values. - -The following data types are not supported - -- GEOMETRY -- POINT -- LINESTRING -- POLYGON -- MULTIPOINT -- MULTILINESTRING -- MULTIPOLYGON -- GEOMETRYCOLLECTION - -## Transaction - -To execute a transaction using `Connection`, use the `setAutoCommit` method in combination with the `commit` and `rollback` methods. - -First, use the `setAutoCommit` method to disable autocommit for a transaction. - -```scala 3 -conn.setAutoCommit(false) -``` - -Use the `commit` method to commit the transaction after some processing. - -```scala 3 -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.commit() -yield -``` - -Or use the `rollback` method to roll back the transaction. - -```scala 3 -for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - result <- statement.executeUpdate() - _ <- conn.rollback() -yield -``` - -If transaction autocommit is disabled using the `setAutoCommit` method, rollback will occur automatically when the connection's Resource is released. - -### transaction isolation level - -ldbc allows you to set the transaction isolation level. - -Transaction isolation levels are set using the `setTransactionIsolation` method. - -The following transaction isolation levels are supported in MySQL - -- READ UNCOMMITTED -- READ COMMITTED -- REPEATABLE READ -- SERIALIZABLE - -See [official documentation](https://dev.mysql.com/doc/refman/8.0/ja/innodb-transaction-isolation-levels.html) for more information on transaction isolation levels in MySQL. - -```scala 3 -import ldbc.connector.Connection.TransactionIsolationLevel - -conn.setTransactionIsolation(TransactionIsolationLevel.REPEATABLE_READ) -``` - -Use the `getTransactionIsolation` method to get the currently set transaction isolation level. - -```scala 3 -for - isolationLevel <- conn.getTransactionIsolation() -yield -``` - -### Savepoint - -For more advanced transaction management, the “Savepoint feature” can be used. This allows you to mark a specific point during a database operation so that if something goes wrong, you can rewind the database state back to that point. This is especially useful for complex database operations or when you need to set a safe point in a long transaction. - -**Feature:** - -- Flexible Transaction Management: Use Savepoint to create a “checkpoint” anywhere within a transaction. State can be returned to that point as needed. -- Error Recovery: Save time and increase efficiency by going back to the last safe Savepoint when an error occurs, rather than starting all over. -- Advanced Control: Multiple Savepoints can be configured for more precise transaction control. Developers can easily implement more complex logic and error handling. - -By taking advantage of this feature, your application will be able to achieve more robust and reliable database operations. - -**Savepoint Settings** - -To set a Savepoint, use the `setSavepoint` method. This method allows you to specify a name for the Savepoint. -If you do not specify a name for the Savepoint, the value generated by the UUID will be set as the default name. - -The `getSavepointName` method can be used to obtain the name of the configured Savepoint. - -※ Since autocommit is enabled by default in MySQL, you must disable autocommit when using Savepoint. Otherwise, all operations will be committed each time, and it will not be possible to roll back transactions using Savepoint. - -```scala 3 -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") -yield savepoint.getSavepointName -``` - -**Rollback of Savepoint** - -To roll back a part of a transaction using Savepoint, rollback is performed by passing Savepoint to the `rollback` method. -If you commit the entire transaction after a partial rollback using Savepoint, the transaction after that Savepoint will not be committed. - -```scala 3 -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.rollback(savepoint) - _ <- conn.commit() -yield -``` - -**Savepoint Release** - -To release a Savepoint, pass the Savepoint to the `releaseSavepoint` method. -After releasing a Savepoint, commit the entire transaction and the transactions after that Savepoint will be committed. - -```scala 3 -for - _ <- conn.setAutoCommit(false) - savepoint <- conn.setSavepoint("savepoint1") - _ <- conn.releaseSavepoint(savepoint) - _ <- conn.commit() -yield -``` - -## Utility Commands - -MySQL has several utility commands. ([see](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase_utility.html)) - -ldbc provides an API to use these commands. - -| Command | Use | Support | -|------------------------|--------------------------------------------------------------------------------------|---------| -| `COM_QUIT` | `Tells the server that the client is requesting the server to close the connection.` | ✅ | -| `COM_INIT_DB` | `Change the default schema for the connection` | ✅ | -| `COM_STATISTICS` | `Obtain an internal status string in readable format.` | ✅ | -| `COM_DEBUG` | `Dump debugging information to the server's standard output` | ❌ | -| `COM_PING` | `Check if the server is alive` | ✅ | -| `COM_CHANGE_USER` | `Change the user of the current connection` | ✅ | -| `COM_RESET_CONNECTION` | `Reset session state` | ✅ | -| `COM_SET_OPTION` | `Set options for the current connection` | ✅ | - -### COM QUIT - -The `COM_QUIT` command is used to tell the server that the client is requesting that the connection be closed. - -In ldbc, the `close` method of `Connection` can be used to close a connection. -Because the `close` method closes the connection, the connection cannot be used in any subsequent process. - -※ The `Connection` uses `Resource` to manage resources. Therefore, there is no need to use the `close` method to release resources. - -```scala 3 -connection.use { conn => - conn.close() -} -``` - -### COM INIT DB - -The `COM_INIT_DB` command is used to change the default schema for the connection. - -In ldbc, the default schema can be changed using the `setSchema` method of `Connection`. - -```scala 3 -connection.use { conn => - conn.setSchema("test") -} -``` - -### COM STATISTICS - -The `COM_STATISTICS` command is used to retrieve internal status strings in readable format. - -In ldbc, you can use the `getStatistics` method of `Connection` to get the internal status string. - -```scala 3 -connection.use { conn => - conn.getStatistics -} -``` - -The statuses that can be obtained are as follows - -- `uptime` : the time since the server was started -- `threads` : number of clients currently connected. -- `questions` : number of queries since the server started -- `slowQueries` : number of slow queries. -- `opens` : number of table opens since the server started. -- `flushTables` : number of tables flushed since the server started. -- `openTables` : number of tables currently open. -- `queriesPerSecondAvg` : average number of queries per second. - -### COM PING - -The `COM_PING` command is used to check if the server is alive. - -In ldbc, you can check if the server is alive using the `isValid` method of `Connection`. -It returns `true` if the server is alive, `false` if not. - -```scala 3 -connection.use { conn => - conn.isValid -} -``` - -### COM CHANGE USER - -The `COM_CHANGE_USER` command is used to change the user of the current connection. -It also resets the following connection states - -- User Variables -- Temporary tables -- Prepared statements -- etc... - -In ldbc, the `changeUser` method of `Connection` can be used to change the user. - -```scala 3 -connection.use { conn => - conn.changeUser("root", "password") -} -``` - -### COM RESET CONNECTION - -`COM_RESET_CONNECTION` is a command to reset the session state. - -`COM_RESET_CONNECTION` is a more lightweight version of `COM_CHANGE_USER`, with almost the same functionality to clean up the session state, but with the following features - -- Do not re-authenticate (no extra client/server exchange to do so). -- Do not close the connection. - -In ldbc, you can reset the session state using the `resetServerState` method of `Connection`. - -```scala 3 -connection.use { conn => - conn.resetServerState -} -``` - -### COM SET OPTION - -`COM_SET_OPTION` is a command to set options for the current connection. - -In ldbc, you can use the `enableMultiQueries` and `disableMultiQueries` methods of `Connection` to set options. - -The `enableMultiQueries` method allows you to run multiple queries at once. -If you use the `disableMultiQueries` method, you will not be able to run multiple queries at once. - -※ It can only be used for batch processing with Insert, Update, and Delete statements; if used with a Select statement, only the results of the first query will be returned. - -```scala 3 -connection.use { conn => - conn.enableMultiQueries *> conn.disableMultiQueries -} -``` - -## Batch commands - -ldbc allows multiple queries to be executed at once using batch commands. -Using batch commands allows multiple queries to be executed at once, thus reducing the number of network round trips. - -To use batch commands, add queries using the `addBatch` method of a `Statement` or `PreparedStatement` and execute the queries using the `executeBatch` method. - -```scala 3 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} -``` - -In the above example, data for `Alice` and `Bob` can be added at once. -The query to be executed would be as follows - -```sql -INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -The return value after executing a batch command is an array of the number of rows affected by each query executed. - -In the above example, one row of data for `Alice` is added and one row of data for `Bob` is added, so the return value is `List(1, 1)`. - -After executing the batch command, the queries that have been added so far by the `addBatch` method will be cleared. - -If you want to clear them manually, use the `clearBatch` method to do so. - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.clearBatch() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - _ <- statement.executeBatch() - yield -} -``` - -In the above example, the data for `Alice` is not added, but the data for `Bob` is. - -### Difference between Statement and PreparedStatement - -The queries executed by the batch command may differ between a `Statement` and a `PreparedStatement`. - -When an INSERT statement is executed in a batch command using a `Statement`, multiple queries are executed at once. -However, if you run an INSERT statement in a batch command using a `PreparedStatement`, a single query will be executed. - -For example, if you run the following query in a batch command, multiple queries will be executed at once because you are using a `Statement`. - -```scala 3 -connection.use { conn => - for - statement <- conn.createStatement() - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Alice', 20)") - _ <- statement.addBatch("INSERT INTO users (name, age) VALUES ('Bob', 30)") - result <- statement.executeBatch() - yield result -} - -// Query to be executed -// INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) VALUES ('Bob', 30); -``` - -However, if the following query is executed in a batch command, one query will be executed because of the use of `PreparedStatement`. - -```scala 3 -connection.use { conn => - for - statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") - _ <- statement.setString(1, "Alice") - _ <- statement.setInt(2, 20) - _ <- statement.addBatch() - _ <- statement.setString(1, "Bob") - _ <- statement.setInt(2, 30) - _ <- statement.addBatch() - result <- statement.executeBatch() - yield result -} - -// Query to be executed -// INSERT INTO users (name, age) VALUES ('Alice', 20), ('Bob', 30); -``` - -This is because if you are using `PreparedStatement`, you can set multiple parameters for a single query by using the `addBatch` method after setting the query parameters. - -## Stored Procedure Execution - -ldbc provides an API for executing stored procedures. - -To execute a stored procedure, use the `prepareCall` method of `Connection` to construct a `CallableStatement`. - -※ The stored procedures used are those described in the [official](https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-statements-callable.html) document. - -```sql -CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) -BEGIN - DECLARE z INT; - SET z = inOutParam + 1; - SET inOutParam = z; - - SELECT inputParam; - - SELECT CONCAT('zyxw', inputParam); -END -``` - -To execute the above stored procedure, the following would be used - -```scala 3 -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - hasResult <- callableStatement.execute() - values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { - for - resultSet <- callableStatement.getResultSet().flatMap { - case Some(rs) => IO.pure(rs) - case None => IO.raiseError(new Exception("No result set")) - } - value <- resultSet.getString(1) - yield value - } - yield values // List(Some("abcdefg"), Some("zyxwabcdefg")) -} -``` - -To get the value of an output parameter (a parameter you specified as OUT or INOUT when you created the stored procedure), JDBC requires you to specify the parameter before statement execution using the various `registerOutputParameter()` methods of the CallableStatement interface. to specify parameters before statement execution, while ldbc will also set parameters during query execution by simply setting them using the `setXXX` method. - -However, ldbc also allows you to specify parameters using the `registerOutputParameter()` method. - -```scala 3 -connection.use { conn => - for - callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) - hasResult <- callableStatement.execute() - value <- callableStatement.getInt(2) - yield value // 2 -} -``` - -※ Note that if you specify an Out parameter with `registerOutParameter`, the value will be set at `Null` for the server if the parameter is not set with the `setXXX` method using the same index value. - -## Unsupported feature - -The ldbc connector is currently an experimental feature. Therefore, the following features are not supported. -We plan to provide the functionality as it becomes available. - -- Connection Pooling -- Failover measures -- etc... diff --git a/docs/src/main/mdoc/en/reference/directory.conf b/docs/src/main/mdoc/en/reference/directory.conf deleted file mode 100644 index 104460f08..000000000 --- a/docs/src/main/mdoc/en/reference/directory.conf +++ /dev/null @@ -1,5 +0,0 @@ -laika.title = Reference -laika.navigationOrder = [ - index.md, - Connector.md -] diff --git a/docs/src/main/mdoc/en/reference/index.md b/docs/src/main/mdoc/en/reference/index.md deleted file mode 100644 index 4c9fa2e68..000000000 --- a/docs/src/main/mdoc/en/reference/index.md +++ /dev/null @@ -1,14 +0,0 @@ -{% - laika.title = Intro - laika.metadata.language = en -%} - -# Reference - -This section contains detailed discussions of ldbc's core abstractions and machinery. - -## Table of Contents - -@:navigationTree { - entries = [ { target = "/en/reference", depth = 2 } ] -} diff --git a/docs/src/main/mdoc/en/tutorial/Connection.md b/docs/src/main/mdoc/en/tutorial/Connection.md deleted file mode 100644 index cf1aea9fa..000000000 --- a/docs/src/main/mdoc/en/tutorial/Connection.md +++ /dev/null @@ -1,93 +0,0 @@ -{% - laika.title = Connection - laika.metadata.language = en -%} - -# Connection - -This chapter describes how to build a connection to connect to a database. - -To connect to a database, a connection must be established. A connection is a resource that manages the connection to the database. A connection provides the resources to initiate a connection to the database, execute a query, and close the connection. - -ldbc connects to the database using either jdbc or ldbc's own connector. Which one to use depends on the dependencies you set up. - -## Use jdbc connector - -First, add the dependencies. - -If you use the JDJD connector, you must also add the MySQL connector. - -```scala -//> dep "@ORGANIZATION@::jdbc-connector:@VERSION@" -//> dep "com.mysql":"mysql-connector-j":"@MYSQL_VERSION@" -``` - -Next, create a data source using `MysqlDataSource`. - -```scala -val ds = new com.mysql.cj.jdbc.MysqlDataSource() -ds.setServerName("127.0.0.1") -ds.setPortNumber(13306) -ds.setDatabaseName("world") -ds.setUser("ldbc") -ds.setPassword("password") -``` - -Create a jdbc connector data source using the data source you created. - -```scala -val datasource = jdbc.connector.MysqlDataSource[IO](ds) -``` - -Finally, a connection is created using a jdbc connector. - -```scala -val connection: Resource[IO, Connection[IO]] = - Resource.make(datasource.getConnection)(_.close()) -``` - -Here we use the Cats Effect `Resource` to close the connection after it has been used. - -## Use ldbc connector - -First, add dependencies. - -```scala -//> dep "@ORGANIZATION@::ldbc-connector:@VERSION@" -``` - -Next, Tracer is provided. ldbc connectors use Tracer to collect telemetry data. These are used to record application traces. - -Here, Tracer is provided using `Tracer.noop`. - -```scala 3 -given Tracer[IO] = Tracer.noop[IO] -``` - -Finally, create a `Connection`. - -```scala -val connection: Resource[IO, Connection[IO]] = - ldbc.connector.Connection[IO]( - host = "127.0.0.1", - port = 3306, - user = "ldbc", - password = Some("password"), - database = Some("ldbc") - ) -``` - -The parameters for setting up a connection are as follows - -| Property | Detail | Required | -|---------------------------|-------------------------------------------------------------------------------|----------| -| `host` | `Database Host Information` | ✅ | -| `port` | `Database Port Information` | ✅ | -| `user` | `Database User Information` | ✅ | -| `password` | `Database password information (default: None)` | ❌ | -| `database` | `Database name information (default: None)` | ❌ | -| `debug` | `Whether to display debugging information or not (default: false)` | ✅ | -| `ssl` | `SSL configuration (default: SSL.None)` | ✅ | -| `socketOptions` | `Specify socket options for TCP/ UDP sockets (default: defaultSocketOptions)` | ✅ | -| `readTimeout` | `Specify timeout period (default: Duration.Inf)` | ✅ | -| `allowPublicKeyRetrieval` | `Whether to retrieve the public key or not (default: false)` | ✅ | diff --git a/docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md b/docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md deleted file mode 100644 index 89b557ce3..000000000 --- a/docs/src/main/mdoc/en/tutorial/Custom-Data-Type.md +++ /dev/null @@ -1,67 +0,0 @@ -{% - laika.title = Custom data type - laika.metadata.language = en -%} - -# Custom data type - -This chapter describes how to use user-specific or unsupported types in table definitions built with ldbc. - -Add a new column to the table definition created in setup. - -```sql -ALTER TABLE user ADD COLUMN status BOOLEAN NOT NULL DEFAULT TRUE; -``` - -## Encoder - -In ldbc, the value to be passed to a statement is represented by an `Encoder`. The `Encoder` is a trait to represent the value to be bound to a statement. - -By implementing `Encoder`, values passed to statements can be expressed as custom types. - -Add `Status` to the user information to represent the user's status. - -```scala 3 -enum Status(val done: Boolean, val name: String): - case Active extends Status(false, "Active") - case InActive extends Status(true, "InActive") -``` -The following code example defines a custom type `Encoder`. - -This allows binding custom types to statements. - -```scala 3 -given Encoder[Status] with - override def encode(status: Status): Boolean = status.done -``` - -Custom types can be bound to statements just like any other parameter. - -```scala -val program1: Executor[IO, Int] = - sql"INSERT INTO user (name, email, status) VALUES (${ "user 1" }, ${ "user@example.com" }, ${ Status.Active })".update -``` - -Now you can bind custom types to statements. - -## Decoder - -In addition to parameters, ldbc provides a `Decoder` to get a unique type from the execution result. - -By implementing the `Decoder`, you can get your own type from the result of statement execution. - -The following code example shows how to use `Decoder.Elem` to get a single data type. - -```scala 3 -given Decoder.Elem[Status] = Decoder.Elem.mapping[Boolean, Status] { - case true => Status.Active - case false => Status.InActive -} -``` - -```scala 3 -val program2: Executor[IO, (String, String, Status)] = - sql"SELECT name, email, status FROM user WHERE id = 1".query[(String, String, Status)].unsafe -``` - -Now you can get a custom type from the execution result of a statement. diff --git a/docs/src/main/mdoc/en/tutorial/Database-Operations.md b/docs/src/main/mdoc/en/tutorial/Database-Operations.md deleted file mode 100644 index 0bf96a3dd..000000000 --- a/docs/src/main/mdoc/en/tutorial/Database-Operations.md +++ /dev/null @@ -1,53 +0,0 @@ -{% - laika.title = Database Operation - laika.metadata.language = en -%} - -# Database Operation - -This section describes database operations. - -Before making a database connection, you need to configure settings such as commit timing and read/write-only. - -## Read Only - -Use the `readOnly` method to initiate a read-only transaction. - -The `readOnly` method can be used to make the processing of a query to be executed read-only. The `readOnly` method can also be used with `insert/update/delete` statements, but it will result in an error at runtime because of the write operation. - -```scala -val read = sql"SELECT 1".query[Int].to[Option].readOnly(connection) -``` - -## Writing - -To write, use the `commit` method. - -The `commit` method can be used to set up the processing of a query to be committed at each query execution. - -```scala -val write = sql"INSERT INTO `table`(`c1`, `c2`) VALUES ('column 1', 'column 2')".update.commit(connection) -``` - -## Transaction - -Use the `transaction` method to initiate a transaction. - -The `transaction` method can be used to combine multiple database connection operations into a single transaction. - -ldbc will build a process to connect to the database in the form of `Executor[F, A]`. Since Executor is a monad, two small programs can be combined into one large program using for comprehensions. - -```scala 3 -val program: Executor[IO, (List[Int], Option[Int], Int)] = - for - result1 <- sql"SELECT 1".query[Int].to[List] - result2 <- sql"SELECT 2".query[Int].to[Option] - result3 <- sql"SELECT 3".query[Int].unsafe - yield (result1, result2, result3) -``` - -The `transaction` method can be used to combine `Executor` programs into a single transaction. - -```scala -val transaction = program.transaction(connection) -``` diff --git a/docs/src/main/mdoc/en/tutorial/Error-Handling.md b/docs/src/main/mdoc/en/tutorial/Error-Handling.md deleted file mode 100644 index 47836b400..000000000 --- a/docs/src/main/mdoc/en/tutorial/Error-Handling.md +++ /dev/null @@ -1,37 +0,0 @@ -{% - laika.title = Error Handling - laika.metadata.language = en -%} - -# Error Handling - -This chapter examines a set of combinators for building programs that trap and handle exceptions. - -## About Exceptions - -Whether an operation succeeds or not depends on unpredictable factors such as the health of the network, the current contents of the table, and the state of the lock. Therefore, we must decide whether to compute everything in a logical OR like `EitherT[Executor, Throwable, A]` or to allow exception propagation until it is explicitly caught. In other words, when an ldbc action (which is converted to a target monad) is executed, an exception may be raised. - -There are three main types of exceptions that are likely to occur - -1. various types of IOExceptions can occur with all types of I/O, and these exceptions tend to be unrecoverable -2. database exceptions usually occur in common situations such as key violations, as a general SQLException that identifies a specific error in vendor-specific SQLState. Error codes must be communicated as lore or discovered by experimentation; there are XOPEN and SQL:2003 standards, but no vendor seems to adhere to these specifications. Some of these errors are recoverable, some are not. -3. ldbc raises InvariantViolation for invalid type mappings, unknown JDBC constants returned by the driver, observed NULL values, and other violations of immutable conditions assumed by ldbc. These exceptions indicate programmer error or driver incompatibility and are generally unrecoverable. - -## Monad errors and derived combinators - -All ldbc monads are derived from the `MonadError[?[_], Throwable]` and provide an Async instance that extends it. This means that Executor and others will have the following primitive operations - -- raiseError: raise an exception (convert Throwable' to `M[A]`) -- handleErrorWith: handle an exception (convert `M[A]` to `M[B]`) -- attempt: catch exception (convert `M[A]` to `M[Either[Throwable, A]]`) - -In other words, any ldbc program can catch an exception simply by adding `attempt`. - -```scala -val program = Executor.pure[IO, Int](1) - -program.attempt -// Executor[IO, Either[Throwable, Int]] -``` - -From the `attempt` and `raiseError` combinators, many other operations can be derived, as described in the Cats documentation. diff --git a/docs/src/main/mdoc/en/tutorial/Logging.md b/docs/src/main/mdoc/en/tutorial/Logging.md deleted file mode 100644 index ff9cb1f2b..000000000 --- a/docs/src/main/mdoc/en/tutorial/Logging.md +++ /dev/null @@ -1,52 +0,0 @@ -{% - laika.title = Logging - laika.metadata.language = en -%} - -# Logging - -ldbc can export execution and error logs of database connections in any format using any logging library. - -The standard logger using Cats Effect's Console is provided and can be used during development. - -```scala 3 -given LogHandler[IO] = LogHandler.console[IO] -``` - -Use `ldbc.dsl.logging.LogHandler` to customize logging using any logging library. - -The following is the standard implementation of logging. ldbc generates the following three types of events on database connection - -- Success: Success of processing -- ProcessingFailure: Error in processing after getting data or before connecting to the database -- ExecFailure: Error in the process of connecting to the database - -Each event is sorted by pattern matching to determine what kind of log to write. - -```scala 3 -def console[F[_]: Console: Sync]: LogHandler[F] = - case LogEvent.Success(sql, args) => - Console[F].println( - s"""Successful Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) - case LogEvent.ProcessingFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed ResultSet Processing: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) - case LogEvent.ExecFailure(sql, args, failure) => - Console[F].errorln( - s"""Failed Statement Execution: - | $sql - | - | arguments = [${ args.mkString(",") }] - |""".stripMargin - ) >> Console[F].printStackTrace(failure) -``` diff --git a/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md b/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md deleted file mode 100644 index 11c3f2292..000000000 --- a/docs/src/main/mdoc/en/tutorial/Parameterized-Queries.md +++ /dev/null @@ -1,112 +0,0 @@ -{% - laika.title = Parameter - laika.metadata.language = en -%} - -# Parameterized Queries - -In this chapter, you will learn how to construct parameterized queries. - -## Adding parameters - -First, create a query with no parameters. - -```scala -sql"SELECT name, email FROM user".query[(String, String)].to[List] -``` - -Next, let's incorporate the query into a method and add a parameter to select only the data that matches the `id` specified by the user. Insert the `id` argument as `$id` into the SQL statement, just as you would interpolate a string. - -```scala -val id = 1 - -sql"SELECT name, email FROM user WHERE id = $id".query[(String, String)].to[List] -``` - -Querying with connections works fine. - -```scala -connection.use { conn => - sql"SELECT name, email FROM user WHERE id = $id" - .query[(String, String)] - .to[List] - .readOnly(conn) -} -``` - -What is happening here? It looks like we are just dropping a string literal into an SQL string, but we are actually building a `PreparedStatement` and the `id` value is eventually set by a call to `setInt`. - -## Multiple parameters - -Multiple parameters work the same way. No surprises. - -```scala -val id = 1 -val email = "alice@example.com" - -connection.use { conn => - sql"SELECT name, email FROM user WHERE id = $id AND email > $email" - .query[(String, String)] - .to[List] - .readOnly(conn) -} -``` - -## Handling IN Clauses - -A common irritation when dealing with SQL literals is the desire to inline a series of arguments into an IN clause, but SQL does not support this concept (nor does JDBC support anything). - -```scala -val ids = NonEmptyList.of(1, 2, 3) - -connection.use { conn => - (sql"SELECT name, email FROM user WHERE" ++ in("id", ids)) - .query[(String, String)] - .to[List] - .readOnly(conn) -} -``` - -Note that the `ids` is `NonEmptyList` since the IN clause must not be empty. - -Executing this query yields the desired result - -ldbc provides several other useful functions. - -- `values` - Creates a VALUES clause. -- `in` - Creates an IN clause. -- `notIn` - Creates a NOT IN clause. -- `and` - Generates an AND clause. -- `or` - Generates an OR clause. -- `whereAnd` - Generates a WHERE clause with multiple conditions enclosed in AND clauses. -- `whereOr` - Generates WHERE clauses for multiple conditions enclosed in OR clauses. -- `set` - Generates a SET clause. -- `orderBy` - Generates an ORDER BY clause. - -## Static parameters - -Although parameters are dynamic, sometimes you may want to use them as parameters but treat them as static values. - -For example, to change the column to be retrieved based on the value received, you can write the following - -```scala -val column = "name" - -sql"SELECT $column FROM user".query[String].to[List] -``` - -Dynamic parameters are handled by `PreparedStatement`, so the query string itself is replaced by `? `. - -Thus, the query will be executed as `SELECT ? FROM user`. - -This makes it difficult to understand the query output in the log, so if you want to treat `$column` as a static value, set `$column` to `${sc(column)}` so that it is directly embedded in the query string. - -```scala -val column = "name" - -sql"SELECT ${sc(column)} FROM user".query[String].to[List] -``` - -This query is executed as `SELECT name FROM user`. - -> `sc(...)` Note that does not escape the passed string. Passing user-supplied data is an injection risk. diff --git a/docs/src/main/mdoc/en/tutorial/Query-Builder.md b/docs/src/main/mdoc/en/tutorial/Query-Builder.md deleted file mode 100644 index 29a97d7c7..000000000 --- a/docs/src/main/mdoc/en/tutorial/Query-Builder.md +++ /dev/null @@ -1,392 +0,0 @@ -{% - laika.title = Query Builder - laika.metadata.language = en -%} - -# Query Builder - -This chapter describes how to build a type-safe query. - -The following dependencies must be set up in your project - -```scala -//> using dep "@ORGANIZATION@::ldbc-query-builder:@VERSION@" -``` - -In ldbc, classes are used to construct queries. - -```scala 3 -import ldbc.query.builder.* - -case class User(id: Int, name: String, email: String) derives Table -``` - -The `User` class inherits from the `Table` trace. Because the `Table` trace inherits from the `Table` class, methods of the `Table` class can be used to construct queries. - -```scala -val query = Table[User] - .select(user => (user.id, user.name, user.email)) - .where(_.email === "alice@example.com") -``` - -## SELECT - -A type-safe way to construct a SELECT statement is to use the `select` method provided by Table. ldbc is implemented to resemble a plain query, making query construction intuitive. It is also easy to see at a glance what kind of query is being constructed. - -To construct a SELECT statement that retrieves only specific columns, simply specify the columns you want to retrieve in the `select` method. - -```scala -val select = Table[User].select(_.id) - -select.statement === "SELECT id FROM user" -``` - -To specify multiple columns, simply specify the columns you wish to retrieve using the `select` method and return a tuple of the specified columns. - -```scala -val select = Table[User].select(user => (user.id, user.name)) - -select.statement === "SELECT id, name FROM user" -``` - -全てのカラムを指定したい場合はTableが提供する`selectAll`メソッドを使用することで構築できます。 - -```scala -val select = Table[User].selectAll - -select.statement === "SELECT id, name, email FROM user" -``` - -If you want to get the number of a specific column, you can construct it by using `count` on the specified column.  - -```scala -val select = Table[User].select(_.id.count) - -select.statement === "SELECT COUNT(id) FROM user" -``` - -### WHERE - -A type-safe way to set a Where condition in a query is to use the `where` method. - -```scala -val where = Table[User].selectAll.where(_.email === "alice@example.com") - -where.statement === "SELECT id, name, email FROM user WHERE email = ?" -``` - -The following is a list of conditions that can be used in the `where` method. - -| Conditions | Statement | -|----------------------------------------|---------------------------------------| -| `===` | `column = ?` | -| `>=` | `column >= ?` | -| `>` | `column > ?` | -| `<=` | `column <= ?` | -| `<` | `column < ?` | -| `<>` | `column <> ?` | -| `!==` | `column != ?` | -| `IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL")` | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | -| `<=>` | `column <=> ?` | -| `IN (value, value, ...)` | `column IN (?, ?, ...)` | -| `BETWEEN (start, end)` | `column BETWEEN ? AND ?` | -| `LIKE (value)` | `column LIKE ?` | -| `LIKE_ESCAPE (like, escape)` | `column LIKE ? ESCAPE ?` | -| `REGEXP (value)` | `column REGEXP ?` | -| `<<` (value) | `column << ?` | -| `>>` (value) | `column >> ?` | -| `DIV (cond, result)` | `column DIV ? = ?` | -| `MOD (cond, result)` | `column MOD ? = ?` | -| `^ (value)` | `column ^ ?` | -| `~ (value)` | `~column = ?` | - -### GROUP BY/Having - -A type-safe way to set the GROUP BY clause in a query is to use the `groupBy` method. - -Using `groupBy` allows you to group data by the value of a column name you specify when you retrieve the data with `select`. - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .groupBy(_._2) - -select.statement === "SELECT id, name FROM user GROUP BY name" -``` - -When grouping, the number of data that can be retrieved with `select` is the number of groups. So, when grouping is done, you can retrieve the values of the columns specified for grouping, or the results of aggregating the column values by group using the provided functions. - -The `having` function allows you to set the conditions under which the data retrieved from the grouping by `groupBy` will be retrieved. - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .groupBy(_._2) - .having(_._1 > 1) - -select.statement === "SELECT id, name FROM user GROUP BY name HAVING id > ?" -``` - -### ORDER BY - -A type-safe way to set the ORDER BY clause in a query is to use the `orderBy` method. - -Using `orderBy` allows you to sort data in ascending or descending order by the value of a column name you specify when retrieving data with `select`. - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .orderBy(_.id) - -select.statement === "SELECT id, name FROM user ORDER BY id" -``` - -If you want to specify ascending/descending order, simply call `asc`/`desc` for the columns, respectively. - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .orderBy(_.id.asc) - -select.statement === "SELECT id, name FROM user ORDER BY id ASC" -``` - -### LIMIT/OFFSET - -A type-safe way to set the LIMIT and OFFSET clauses in a query is to use the `limit`/`offset` methods. - -Setting `limit` allows you to set an upper limit on the number of rows of data to retrieve when `select` is executed, while setting `offset` allows you to specify the number of rows of data to retrieve. - -```scala -val select = Table[User] - .select(user => (user.id, user.name)) - .limit(1) - .offset(1) - -select.statement === "SELECT id, name FROM user LIMIT ? OFFSET ?" -``` - -## JOIN/LEFT JOIN/RIGHT JOIN - -A type-safe way to set a Join in a query is to use the `join`/`leftJoin`/`rightJoin` methods. - -The following definition is used as a sample for Join. - -```scala 3 -case class User(id: Int, name: String, email: String) derives Table -case class Product(id: Int, name: String, price: BigDecimal) derives Table -case class Order( - id: Int, - userId: Int, - productId: Int, - orderDate: LocalDateTime, - quantity: Int -) derives Table - -val userTable = Table[User] -val productTable = Table[Product] -val orderTable = Table[Order] -``` - -First, if you want to perform a simple join, use `join`. -The first argument of `join` is the table to be joined, and the second argument is a function that compares the source table with the columns of the table to be joined. This corresponds to the ON clause in the join. - -After the join, the `select` will specify columns from the two tables. - -```scala -val join = userTable.join(orderTable)((user, order) => user.id === order.userId) - .select((user, order) => (user.name, order.quantity)) - -join.statement = "SELECT user.`name`, order.`quantity` FROM user JOIN order ON user.id = order.user_id" -``` - -Next, if you want to perform a Left Join, which is a left outer join, use `leftJoin`. -The implementation itself is the same as for a simple Join, only `join` is changed to `leftJoin`. - -```scala 3 -val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) - .select((user, order) => (user.name, order.quantity)) - -join.statement = "SELECT user.`name`, order.`quantity` FROM user LEFT JOIN order ON user.id = order.user_id" -``` - -The difference from a simple Join is that when using `leftJoin`, the records retrieved from the table to be joined may be NULL. - -Therefore, in ldbc, all records in the column retrieved from the table passed to `leftJoin` will be of type Option. - -```scala 3 -val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) - .select((user, order) => (user.name, order.quantity)) // (String, Option[Int]) -``` - -Next, if you want to perform a right join, which is a right outer join, use `rightJoin`. -The implementation itself is the same as for a simple join, only the `join` is changed to `rightJoin`. - -```scala 3 -val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) - .select((order, user) => (order.quantity, user.name)) - -join.statement = "SELECT order.`quantity`, user.`name` FROM order RIGHT JOIN user ON order.user_id = user.id" -``` - -The difference from a simple Join is that when using `rightJoin`, the records retrieved from the join source table may be NULL. - -Therefore, in ldbc, all records in the columns retrieved from the join source table using `rightJoin` will be of type Option. - -```scala 3 -val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) - .select((order, user) => (order.quantity, user.name)) // (Option[Int], String) -``` - -If multiple joins are desired, this can be accomplished by calling any Join method in the method chain. - -```scala 3 -val join = - (productTable join orderTable)((product, order) => product.id === order.productId) - .rightJoin(userTable)((_, order, user) => order.userId === user.id) - .select((product, order, user) => (product.name, order.quantity, user.name)) // (Option[String], Option[Int], String)] - -join.statement = - """ - |SELECT - | product.`name`, - | order.`quantity`, - | user.`name` - |FROM product - |JOIN order ON product.id = order.product_id - |RIGHT JOIN user ON order.user_id = user.id - |""".stripMargin -``` - -Note that a `rightJoin` join with multiple joins will result in NULL-acceptable access to all records retrieved from the previously joined table, regardless of what the previous join was. - -## INSERT - -A type-safe way to construct an INSERT statement is to use the following methods provided by Table. - -- insert -- insertInto -- += -- ++= - -**insert** - -The `insert` method is passed a tuple of data to insert. The tuples must have the same number and type of properties as the model. Also, the order of the inserted data must be in the same order as the model properties and table columns. - -```scala 3 -val insert = user.insert((1, "name", "email@example.com")) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" -``` - -If you want to insert multiple data, you can construct it by passing multiple tuples to the `insert` method. - -```scala 3 -val insert = user.insert((1, "name 1", "email+1@example.com"), (2, "name 2", "email+2@example.com")) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -**insertInto** - -The `insert` method will insert data into all columns the table has, but if you want to insert data only into specific columns, use the `insertInto` method. - -This can be used to exclude data insertion into columns with AutoIncrement or Default values. - -```scala 3 -val insert = user.insertInto(user => (user.name, user.email)).values(("name 3", "email+3@example.com")) - -insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?)" -``` - -If you want to insert multiple data, you can construct it by passing an array of tuples to `values`. - -```scala 3 -val insert = user.insertInto(user => (user.name, user.email)).values(List(("name 4", "email+4@example.com"), ("name 5", "email+5@example.com"))) - -insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?), (?, ?)" -``` - -**+=** - -The `+=` method can be used to construct an INSERT statement using a model. Note that when using a model, data is inserted into all columns. - -```scala 3 -val insert = user += User(6, "name 6", "email+6@example.com") - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" -``` - -**++=** - -Use the `++=` method if you want to insert multiple data using the model. - -```scala 3 -val insert = user ++= List(User(7, "name 7", "email+7@example.com"), User(8, "name 8", "email+8@example.com")) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" -``` - -### ON DUPLICATE KEY UPDATE - -Inserting a row with an ON DUPLICATE KEY UPDATE clause will cause an UPDATE of the old row if it has a duplicate value in a UNIQUE index or PRIMARY KEY. - -The ldbc way to accomplish this is to use `onDuplicateKeyUpdate` for `Insert`. - -```scala -val insert = user.insert((9, "name", "email+9@example.com")).onDuplicateKeyUpdate(v => (v.name, v.email)) - -insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `email` = new_user.`email`" -``` - -## UPDATE - -A type-safe way to construct an UPDATE statement is to use the `update` method provided by Table. - -The first argument of the `update` method is the name of the model property, not the column name of the table, and the second argument is the value to be updated. The type of the value passed as the second argument must be the same as the type of the property specified in the first argument. - -```scala -val update = user.update("name", "update name") - -update.statement === "UPDATE user SET name = ?" -``` - -If a property name that does not exist is specified as the first argument, a compile error occurs. - -```scala 3 -val update = user.update("hoge", "update name") // Compile error -``` - -If you want to update multiple columns, use the `set` method. - -```scala 3 -val update = user.update("name", "update name").set("email", "update-email@example.com") - -update.statement === "UPDATE user SET name = ?, email = ?" -``` - -You can also prevent the `set` method from generating queries based on conditions. - -```scala 3 -val update = user.update("name", "update name").set("email", "update-email@example.com", false) - -update.statement === "UPDATE user SET name = ?" -``` - -You can also use a model to construct the UPDATE statement. Note that if you use a model, all columns will be updated. - -```scala 3 -val update = user.update(User(1, "update name", "update-email@example.com")) - -update.statement === "UPDATE user SET id = ?, name = ?, email = ?" -``` - -## DELETE - -A type-safe way to construct a DELETE statement is to use the `delete` method provided by Table. - -```scala -val delete = user.delete - -delete.statement === "DELETE FROM user" -``` diff --git a/docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md b/docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md deleted file mode 100644 index 441338a12..000000000 --- a/docs/src/main/mdoc/en/tutorial/Schema-Code-Generation.md +++ /dev/null @@ -1,205 +0,0 @@ -{% - laika.title = Schema code generation - laika.metadata.language = en -%} - -# Schema code generation - -This chapter describes how to automatically generate ldbc table definitions from SQL files. - -The following dependencies must be set up in your project - -```scala 3 -addSbtPlugin("@ORGANIZATION@" % "ldbc-plugin" % "@VERSION@") -``` - -## Generate - -Enable the plugin for the project. - -```sbt -lazy val root = (project in file(".")) - .enablePlugins(Ldbc) -``` - -Specify the SQL file to be analyzed as an array. - -```sbt -Compile / parseFiles := List(baseDirectory.value / "test.sql") -``` - -**List of keys that can be set by enabling the plugin** - -| Key | Detail | -|----------------------|------------------------------------------------------------------------| -| `parseFiles` | `List of SQL files to be analyzed` | -| `parseDirectories` | `Specify SQL files to be parsed by directory` | -| `excludeFiles` | `List of file names to exclude from analysis` | -| `customYamlFiles` | `List of yaml files for customizing Scala types and column data types` | -| `classNameFormat` | `Value specifying the format of the class name` | -| `propertyNameFormat` | `Value specifying the format of the property name in the Scala model` | -| `ldbcPackage` | `Value specifying the package name of the generated file` | - -The SQL file to be parsed must always begin with a Create or Use statement for the database. ldbc parses the file one file at a time, generates table definitions, and stores the list of tables in the database model. -This is because it is necessary to tell which database the table belongs to. - -```sql -CREATE DATABASE `location`; - -USE `location`; - -DROP TABLE IF EXISTS `country`; -CREATE TABLE country ( - `id` BIGINT AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(255) NOT NULL, - `code` INT NOT NULL -); -``` - -The SQL file to be analyzed should contain only Create/Use statements for the database or Create/Drop statements for table definitions. - -## Generated Code - -When the sbt project is started and compiled, model classes generated based on the SQL file to be analyzed and table definitions are generated under the target of the sbt project. - -```shell -sbt compile -``` - -The code generated from the above SQL file will look like this - -```scala 3 -package ldbc.generated.location - -import ldbc.schema.* - -case class Country( - id: Long, - name: String, - code: Int -) - -object Country: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -If the SQL file has been modified or the cache has been removed by running the clean command, Compile will generate the code again. If the SQL file has been modified or the cache has been deleted by executing the clean command, the code will be generated again when Compile is executed. -If you want to generate the code again without using the cache, execute the command `generateBySchema`. This command does not use the cache and always generates code. - -```shell -sbt generateBySchema -``` - -## Customization - -There may be times when you want to convert the type of code generated from an SQL file to something else. This can be done by passing `customYamlFiles` with the yml files to be customized. - -```sbt -Compile / customYamlFiles := List( - baseDirectory.value / "custom.yml" -) -``` - -The format of the yml file should be as follows - -```yaml -database: - name: '{Database Name}' - tables: - - name: '{Table Name}' - columns: # Optional - - name: '{Column Name}' - type: '{Scala type you want to change}' - class: # Optional - extends: - - '{Package paths such as trait that you want model classes to inherit}' // package.trait.name - object: # Optional - extends: - - '{The package path, such as trait, that you want the object to inherit.}' - - name: '{Table Name}' - ... -``` - -The `database` must be the name of the database listed in the SQL file to be analyzed. The table name must be the name of a table belonging to the database listed in the SQL file to be parsed. - -The `columns` field should be a string containing the name of the column whose type you want to change and the Scala type you want to change. Columns` can have multiple values, but the column name in `name` must belong to the target table. -Also, the Scala type to be converted must be one that is supported by the column's Data type. If you want to specify an unsupported type, you must pass a trait, abstract class, etc. that is configured to do implicit type conversion for `object`. - -See [here](/en/tutorial/Schema.md#data-type) for types supported by Data type and [here](/en/tutorial/Schema.md#custom-data-types) for how to set unsupported types. - -To convert an Int type to the user's own type, CountryCode, implement the following `CustomMapping`trait - -```scala 3 -trait CountryCode: - val code: Int -object Japan extends CountryCode: - override val code: Int = 1 - -trait CustomMapping: // 任意の名前 - given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode] -``` - -Set the `CustomMapping`trait that you have implemented in the yml file for customization, and convert the target column type to CountryCode. - -```yaml -database: - name: 'location' - tables: - - name: 'country' - columns: - - name: 'code' - type: 'Country.CountryCode' // CustomMapping is mixed in with the Country object so that it can be retrieved from there. - object: - extends: - - '{package.name.}CustomMapping' -``` - -The code generated by the above configuration will be as follows, allowing users to generate model and table definitions with their own types. - -```scala 3 -case class Country( - id: Long, - name: String, - code: Country.CountryCode -) - -object Country extends /*{package.name.}*/CustomMapping: - val table = Table[Country]("country")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("code", INT) - ) -``` - -The database model is also automatically generated from SQL files. - -```scala 3 -package ldbc.generated.location - -import ldbc.schema.* - -case class LocationDatabase( - schemaMeta: Option[String] = None, - catalog: Option[String] = Some("def"), - host: String = "127.0.0.1", - port: Int = 3306 -) extends Database: - - override val databaseType: Database.Type = Database.Type.MySQL - - override val name: String = "location" - - override val schema: String = "location" - - override val character: Option[Character] = None - - override val collate: Option[Collate] = None - - override val tables = Set( - Country.table - ) -``` diff --git a/docs/src/main/mdoc/en/tutorial/Schema.md b/docs/src/main/mdoc/en/tutorial/Schema.md deleted file mode 100644 index e9b51d31b..000000000 --- a/docs/src/main/mdoc/en/tutorial/Schema.md +++ /dev/null @@ -1,485 +0,0 @@ -{% - laika.title = Schema - laika.metadata.language = en -%} - -# Schema - -This chapter describes how to work with database schemas in Scala code, especially how to manually write a schema, which is useful when starting to write an application without an existing database. If you already have a schema in your database, you can skip this step using Code Generator. - -The following dependencies must be set up for your project - -```scala -//> using dep "@ORGANIZATION@::ldbc-schema:@VERSION@" -``` - -The following code example assumes the following import - -```scala 3 -import ldbc.schema.* -import ldbc.schema.attribute.* -``` - -ldbc maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in the order of definition. Table definitions are very similar to the structure of a Create statement. This makes the construction of table definitions intuitive for the user. - -ldbc uses these table definitions for a variety of purposes. Generating type-safe queries, generating documents, etc. - -```scala 3 -case class User( - id: Int, - name: String, - email: String, -) - -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), // `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(50)), // `name` VARCHAR(50) NOT NULL, - column("email", VARCHAR(100)), // `email` VARCHAR(100) NOT NULL, -) // ); -``` - -All columns are defined by the column method. Each column has a column name, data type, and attributes. The following primitive types are supported by default and are ready to use - -- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` -- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` -- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` -- String -- Boolean -- java.time.* - -Nullable columns are represented by `Option[T]`, where T is one of the supported primitive types; note that any column that is not of type Option is Not Null. - -## Data Type - -The mapping of the Scala type of property that a model has to the data type that a column has requires that the defined data type supports the Scala type. Attempting to assign an unsupported type will result in a compile error. - -The Scala types supported by the data type are listed in the table below. - -| Data Type | Scala Type | -|--------------|-------------------------------------------------------------------------------------------------| -| `BIT` | `Byte, Short, Int, Long` | -| `TINYINT` | `Byte, Short` | -| `SMALLINT` | `Short, Int` | -| `MEDIUMINT` | `Int` | -| `INT` | `Int, Long` | -| `BIGINT` | `Long, BigInt` | -| `DECIMAL` | `BigDecimal` | -| `FLOAT` | `Float` | -| `DOUBLE` | `Double` | -| `CHAR` | `String` | -| `VARCHAR` | `String` | -| `BINARY` | `Array[Byte]` | -| `VARBINARY` | `Array[Byte]` | -| `TINYBLOB` | `Array[Byte]` | -| `BLOB` | `Array[Byte]` | -| `MEDIUMBLOB` | `Array[Byte]` | -| `LONGBLOB` | `Array[Byte]` | -| `TINYTEXT` | `String` | -| `TEXT` | `String` | -| `MEDIUMTEXT` | `String` | -| `DATE` | `java.time.LocalDate` | -| `DATETIME` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime` | -| `TIMESTAMP` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime` | -| `TIME` | `java.time.LocalTime` | -| `YEAR` | `java.time.Instant, java.time.LocalDate, java.time.Year` | -| `BOOLEA` | `Boolean` | - -**Note on working with integer types** - -It should be noted that the range of data that can be handled, depending on whether it is signed or unsigned, does not fit within the Scala types. - -| Data Type | Signed Range | Unsigned Range | Scala Type | Range | -|-------------|----------------------------------------------|----------------------------|------------------|----------------------------------------------------------------------| -| `TINYINT` | `-128 ~ 127` | `0 ~ 255` | `Byte
Short` | `-128 ~ 127
-32768~32767` | -| `SMALLINT` | `-32768 ~ 32767` | `0 ~ 65535` | `Short
Int` | `-32768~32767
-2147483648~2147483647` | -| `MEDIUMINT` | `-8388608 ~ 8388607` | `0 ~ 16777215` | `Int` | `-2147483648~2147483647` | -| `INT` | `-2147483648 ~ 2147483647` | `0 ~ 4294967295` | `Int
Long` | `-2147483648~2147483647
-9223372036854775808~9223372036854775807` | -| `BIGINT` | `-9223372036854775808 ~ 9223372036854775807` | `0 ~ 18446744073709551615` | `Long
BigInt` | `-9223372036854775808~9223372036854775807
...` | - -To work with user-defined proprietary or unsupported types, see Custom Data Types. - -## Attributes - -Columns can be assigned various attributes. - -- `AUTO_INCREMENT`. - Create a DDL statement to mark a column as an auto-increment key when documenting SchemaSPY. - MySQL cannot return columns that are not AutoInc when inserting data. Therefore, if necessary, ldbc will check to see if the return column is properly marked as AutoInc. -- `PRIMARY_KEY`. - Mark the column as a primary key when creating DDL statements or SchemaSPY documents. -- `UNIQUE_KEY`. - Marks a column as a unique key when creating a DDL statement or SchemaSPY document. -- `COMMENT`. - Marks a column as a comment when creating a DDL statement or SchemaSPY document. - -## Setting Keys - -MySQL allows you to set various keys for your tables, such as Unique keys, Index keys, foreign keys, etc. Let's look at how to set these keys in a table definition built with ldbc. - -### PRIMARY KEY - -A primary key is an item that uniquely identifies data in MySQL. When a column has a primary key constraint, it can only contain values that do not duplicate the values of other data. It also cannot contain NULLs. As a result, only one piece of data in the table can be identified by looking up the value of a column with a primary key constraint set. - -In ldbc, this primary key constraint can be set in two ways. - -1. set as an attribute of column method -2. set by keySet method of table - -**Setting as an attribute of the column method**. - -It is very easy to set a column method attribute by simply passing `PRIMARY_KEY` as the third or later argument of the column method. This allows you to set the `id` column as the primary key in the following cases. - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) -``` - -Set by keySet method of **table**. - -ldbc table definitions have a method called `keySet`, where you can set the column you want to set as the primary key by passing `PRIMARY_KEY` as the column name. - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => PRIMARY_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`) -// ) -``` - -The `PRIMARY_KEY` method can be set to the following parameters in addition to the columns. - -- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH -- `Index Option` ldbc.schema.Index.IndexOption - -#### composite key (primary key) - -Not only one column, but also multiple columns can be combined as a primary key. You can set up a composite primary key by simply passing `PRIMARY_KEY` with the columns you want as primary keys. - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => PRIMARY_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// PRIMARY KEY (`id`, `name`) -// ) -``` - -Compound keys can only be set with `PRIMARY_KEY` in the `keySet` method. If you set multiple attributes in the column method as shown below, each attribute will be set as a primary key, not as a compound key. - -In ldbc, setting multiple `PRIMARY_KEY`s in a table definition does not cause a compile error. However, if the table definition is used in query generation, document generation, etc., an error will occur. This is due to the restriction that only one PRIMARY KEY can be set per table. - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50), PRIMARY_KEY), - column("email", VARCHAR(100)) -) - -// CREATE TABLE `user` ( -// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, -// ) -``` - -### UNIQUE KEY - -A unique key is an item that uniquely identifies data in MySQL. When a uniqueness constraint is set on a column, the column can only contain values that do not duplicate the values of other data. - -In ldbc, this uniqueness constraint can be set in two ways. 1. - -1. as an attribute of the column method -2. in the keySet method of table - -**Setting as an attribute of the column method**. - -It is very easy to set a column method as an attribute by simply passing `UNIQUE_KEY` as the third or later argument of the column method. This allows you to set the `id` column as a unique key in the following cases. - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) -``` - -**Set by keySet method of table** - -The ldbc table definition has a method called `keySet` where you can set a column as a unique key by passing `UNIQUE_KEY` as the column name you want to set as a unique key. - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => UNIQUE_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`) -// ) -``` - -The `UNIQUE_KEY` method accepts the following parameters in addition to the columns. - -- `Index Name` String -- `Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH -- `Index Option` ldbc.schema.Index.IndexOption - -#### composite key (unique key) - -You can set not only one column as a unique key, but also multiple columns as a combined unique key. You can set up a composite unique key by simply passing `UNIQUE_KEY` with multiple columns that you want to set as unique keys. - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => UNIQUE_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// UNIQUE KEY (`id`, `name`) -// ) -``` - -Compound keys can only be set with `UNIQUE_KEY` in the `keySet` method. If you set multiple keys as attributes in the column method, each will be set as a unique key, not as a compound key. - -### INDEX KEY - -An index key is an “index” in MySQL to efficiently retrieve the desired record. - -In ldbc, this index can be set in two ways. - -1. as an attribute of the column method -2. by using the keySet method of table. - -**Set as an attribute of the column method** - -It is very easy to set a column method as an attribute, just pass `INDEX_KEY` as the third argument or later of the column method. This allows you to set the `id` column as an index in the following cases - -```scala 3 -column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) -``` - -**Set by keySet method of table** - -The ldbc table definition has a method called `keySet`, where you can set a column as an index key by passing the column you want to set as an index to `INDEX_KEY`. - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => INDEX_KEY(table.id)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`) -// ) -``` - -The `INDEX_KEY` method accepts the following parameters in addition to the columns. - -- `Index Name` String -- Index Type` ldbc.schema.Index.Type.BTREE or ldbc.schema.Index.Type.HASH -- `Index Option` ldbc.schema.Index.IndexOption - -#### composite key (index key) - -You can set not only one column but also multiple columns as index keys as a combined index key. You can set up a composite index by simply passing `INDEX_KEY` with multiple columns that you want to set as index keys. - -```scala 3 -val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - .keySet(table => INDEX_KEY(table.id, table.name)) - -// CREATE TABLE `user` ( -// ..., -// INDEX KEY (`id`, `name`) -// ) -``` - -Compound keys can only be set with `INDEX_KEY` in the `keySet` method. If you set multiple columns as attributes of the `column` method, they will each be set as an index key, not as a composite index. - -### FOREIGN KEY - -A foreign key is a data integrity constraint (referential integrity constraint) in MySQL. A column set to a foreign key can only have values that exist in the columns of the referenced table. - -In ldbc, this foreign key constraint can be set by using the keySet method of table. - -```scala 3 -val user = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - ... -) - .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id))) - -// CREATE TABLE `order` ( -// ..., -// FOREIGN KEY (user_id) REFERENCES `user` (id), -// ) -``` - -The `FOREIGN_KEY` method accepts the following parameters in addition to column and reference values. - -- `Index Name` String - -Foreign key constraints can be used to set the behavior of the parent table on delete and update. The `REFERENCE` method provides `onDelete` and `onUpdate` methods that can be used to set these parameters. - -Values that can be set can be obtained from `ldbc.schema.Reference.ReferenceOption`. - -```scala 3 -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - ... -) - .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id).onDelete(Reference.ReferenceOption.RESTRICT))) - -// CREATE TABLE `order` ( -// ..., -// FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT -// ) -``` - -Possible values are - -- `RESTRICT`: deny delete or update operations on the parent table. -- `CASCADE`: delete or update a row from the parent table and automatically delete or update the matching row in the child table. -- `SET_NULL`: deletes or updates a row from the parent table and sets a foreign key column in the child table to NULL. -- `NO_ACTION`: Standard SQL keyword. In MySQL, equivalent to RESTRICT. -- `SET_DEFAULT`: This action is recognized by the MySQL parser, but both InnoDB and NDB will reject table definitions containing an ON DELETE SET DEFAULT or ON UPDATE SET DEFAULT clause. - -#### composite key (foreign key) - -Not only one column, but also multiple columns can be combined as a foreign key. Simply pass multiple columns to `FOREIGN_KEY` to be set as foreign keys as a compound foreign key. - -```scala 3 -val user = Table[User]("user")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) -) - -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - column("user_email", VARCHAR(100)) - ... -) - .keySet(table => FOREIGN_KEY((table.userId, table.userEmail), REFERENCE(user, (user.id, user.email)))) - -// CREATE TABLE `user` ( -// ..., -// FOREIGN KEY (`user_id`, `user_email`) REFERENCES `user` (`id`, `email`) -// ) -``` - -### constraint name - -MySQL allows you to give arbitrary names to constraints by using CONSTRAINT. The constraint name must be unique per database. - -Since ldbc provides the CONSTRAINT method, you can set constraints such as key constraints by simply passing them to the CONSTRAINT method. - -```scala 3 -val order = Table[Order]("order")( - column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), - column("user_id", VARCHAR(50)) - ... -) - .keySet(table => CONSTRAINT("fk_user_id", FOREIGN_KEY(table.userId, REFERENCE(user, user.id)))) - -// CREATE TABLE `order` ( -// ..., -// CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -// ) -``` - -## Custom data types - -The way to use user-specific or unsupported types is to tell them what type to treat the column data type as; DataType provides a `mapping` method that can be used to set this up as an implicit type conversion. - -```scala 3 -case class User( - id: Int, - name: User.Name, - email: String, -) - -object User: - - case class Name(firstName: String, lastName: String) - - given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] - - val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("name", VARCHAR(50)), - column("email", VARCHAR(100)) - ) -``` - -ldbc does not allow multiple columns to be merged into a single property of the model, since the purpose of ldbc is to provide a one-to-one mapping between model and table, and to type-safe the table definitions in the database. - -Therefore, it is not allowed to have different number of properties in a table definition and in a model. The following implementation will result in a compile error - -```scala 3 -case class User( - id: Int, - name: User.Name, - email: String, -) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("first_name", VARCHAR(50)), - column("last_name", VARCHAR(50)), - column("email", VARCHAR(100)) - ) -``` - -If you wish to implement the above, please consider the following implementation. - -```scala 3 -case class User( - id: Int, - firstName: String, - lastName: String, - email: String, -): - - val name: User.Name = User.Name(firstName, lastName) - -object User: - - case class Name(firstName: String, lastName: String) - - val table = Table[User]("user")( - column("id", INT, AUTO_INCREMENT), - column("first_name", VARCHAR(50)), - column("last_name", VARCHAR(50)), - column("email", VARCHAR(100)) - ) -``` diff --git a/docs/src/main/mdoc/en/tutorial/Selecting-Data.md b/docs/src/main/mdoc/en/tutorial/Selecting-Data.md deleted file mode 100644 index d67dc9887..000000000 --- a/docs/src/main/mdoc/en/tutorial/Selecting-Data.md +++ /dev/null @@ -1,147 +0,0 @@ -{% - laika.title = Data Select - laika.metadata.language = en -%} - -# Data Select - -This chapter describes how to select data using the ldbc data set. - -## Loading rows into a collection - -For the first query, let's aim for a low-level query, select a few users to list, and print out the first few cases. There are several steps here, so I will note the types along the way. - -```scala -sql"SELECT name FROM user" - .query[String] // Query[IO, String] - .to[List] // Executor[IO, List[String]] - .readOnly(conn) // IO[List[String]] - .unsafeRunSync() // List[String] - .foreach(println) // Unit -``` - -Let's break this down a bit. - -- `sql “SELECT name FROM user”.query[String]` is a single-column query that defines `Query[IO, String]` and maps each row returned to a String. This query is a single-column query that maps each row returned to a String. -- `.to[List]` is a convenience method that accumulates rows into a list, in this case `Executor[IO, List[String]]`. This method works for all collection types with CanBuildFrom. -- `readOnly(conn)` generates `IO[List[String]]`, which when executed will output a normal Scala `List[String]`. -- `unsafeRunSync()` executes the IO monad and gets the result. This is used to execute the IO monad and get the result. -- `foreach(println)` prints out each element of the list. - -## Multiple column query - -Of course, multiple columns can be selected and mapped to tuples. - -```scala -sql"SELECT name, email FROM user" - .query[(String, String)] // Query[IO, (String, String)] - .to[List] // Executor[IO, List[(String, String)]] - .readOnly(conn) // IO[List[(String, String)]] - .unsafeRunSync() // List[(String, String)] - .foreach(println) // Unit -``` - -## Mapping to classes - -ldbc also allows you to select multiple columns and map them to classes. This is an example of defining a `User` class and mapping the query results to the `User` class. - -```scala 3 -case class User(id: Long, name: String, email: String) - -sql"SELECT id, name, email FROM user" - .query[User] // Query[IO, User] - .to[List] // Executor[IO, List[User]] - .readOnly(conn) // IO[List[User]] - .unsafeRunSync() // List[User] - .foreach(println) // Unit -``` - -The fields of the class must match the column names of the query. This means that the fields of the `User` class are `id`, `name`, and `email`, so the query column names must be `id`, `name`, and `email`. - -![Selecting Data](../../img/data_select.png) - -Let's see how to select data from multiple tables using `Join` and other methods. - -This is an example of defining `City`, `Country`, and `CityWithCountry` classes, respectively, and mapping the results of a query that `Joins` the `city` and `country` tables to the `CityWithCountry` class. - -```scala 3 -case class City(id: Long, name: String) -case class Country(code: String, name: String, region: String) -case class CityWithCountry(coty: City, country: Country) - -sql""" - SELECT - city.id, - city.name, - country.code, - country.name, - country.region - FROM city - JOIN country ON city.country_code = country.code -""" - .query[CityWithCountry] // Query[IO, CityWithCountry] - .to[List] // Executor[IO, List[CityWithCountry]] - .readOnly(conn) // IO[List[CityWithCountry]] - .unsafeRunSync() // List[CityWithCountry] - .foreach(println) // Unit -``` - -We mentioned earlier that the fields of the class must match the column names of the query. -In this case, the fields of the `City` class are `id` and `name`, and the fields of the `Country` class are `code`, `name`, and `region`, so the query column names are `id`, `name`, `code`, `name`, and `region means that the query column names are - -If you do a `Join`, each column must be specified with a table name to indicate which table the column is from. -In this example, the columns are specified as `city.id`, `city.name`, `country.code`, `country.name`, and `country.region`. - -In ldbc, this is how `table name`. `column name` to `class name`. By mapping `field names` to `field names`, data from multiple tables can be mapped to nested classes. - -![Selecting Data](../../img/data_multi_select.png) - -In ldbc, when performing a `Join` to retrieve data from multiple tables, it is possible to map to a class `Tuple` instead of only a single class. - -```scala 3 -case class City(id: Long, name: String) -case class Country(code: String, name: String, region: String) - -sql""" - SELECT - city.id, - city.name, - country.code, - country.name, - country.region - FROM city - JOIN country ON city.country_code = country.code -""" - .query[(City, Country)] - .to[List] - .readOnly(conn) - .unsafeRunSync() - .foreach(println) -``` - -In this example, the `City` and `Country` classes are mapped to `Tuple`. - -It is important to note that, unlike the previous example, the `Table Name`. `Column Name` to `Class Name`. Field Name`, the table name is the class name. - -Therefore, there is a restriction on this mapping: table names and class names must be equivalent. In other words, if you shorten the table name from `city` to `c`, etc., using aliases, etc., the class name must also be `C`. - -```scala 3 -case class C(id: Long, name: String) -case class CT(code: String, name: String, region: String) - -sql""" - SELECT - c.id, - c.name, - ct.code, - ct.name, - ct.region - FROM city AS c - JOIN country AS ct ON c.country_code = ct.code -""" - .query[(City, Country)] - .to[List] - .readOnly(conn) - .unsafeRunSync() - .foreach(println) -``` diff --git a/docs/src/main/mdoc/en/tutorial/Setup.md b/docs/src/main/mdoc/en/tutorial/Setup.md deleted file mode 100644 index e714a0998..000000000 --- a/docs/src/main/mdoc/en/tutorial/Setup.md +++ /dev/null @@ -1,183 +0,0 @@ -{% - laika.title = Setup - laika.metadata.language = en -%} - -# Setup - -Welcome to the wonderful world of ldbc! In this section we will help you get everything set up. - -## Database Setup - -First, start the database using Docker. Use the following code to start the database - -```yaml -services: - mysql: - image: mysql:@MYSQL_VERSION@ - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 -``` - -Next, initialize the database. - -Create the database as shown in the code below. - -```sql -CREATE DATABASE IF NOT EXISTS sandbox_db; -``` - -Next, tables are created. - -```sql -CREATE TABLE IF NOT EXISTS `user` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(50) NOT NULL, - `email` VARCHAR(100) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS `product` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(100) NOT NULL, - `price` DECIMAL(10, 2) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) - -CREATE TABLE IF NOT EXISTS `order` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `user_id` INT NOT NULL, - `product_id` INT NOT NULL, - `order_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `quantity` INT NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES `user` (id), - FOREIGN KEY (product_id) REFERENCES `product` (id) -) -``` - -Insert data into each table. - -```sql -INSERT INTO user (name, email) VALUES - ('Alice', 'alice@example.com'), - ('Bob', 'bob@example.com'), - ('Charlie', 'charlie@example.com'); - -INSERT INTO product (name, price) VALUES - ('Laptop', 999.99), - ('Mouse', 19.99), - ('Keyboard', 49.99), - ('Monitor', 199.99); - -INSERT INTO `order` (user_id, product_id, quantity) VALUES - (1, 1, 1), -- Alice ordered 1 Laptop - (1, 2, 2), -- Alice ordered 2 Mice - (2, 3, 1), -- Bob ordered 1 Keyboard - (3, 4, 1); -- Charlie ordered 1 Monitor -``` - -## Scala Setup - -The tutorial will use [Scala CLI](https://scala-cli.virtuslab.org/). Therefore, you will need to install the Scala CLI. - -```bash -brew install Virtuslab/scala-cli/scala-cli -``` - -**Execute with Scala CLI** - -The database setup described earlier can be performed using the Scala CLI. The following commands can be used to perform this setup. - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -### First Program - -To begin, create a new project with ldbc as a dependency. - -```scala -//> using scala "@SCALA_VERSION@" -//> using dep "@ORGANIZATION@::ldbc-dsl:@VERSION@" -``` - -Before using ldbc, some symbols need to be imported. For convenience, we will use package import here. This will give us the symbols most commonly used when working with high-level APIs. - -```scala -import ldbc.dsl.io.* -``` - -Let's bring Cats too. - -```scala -import cats.syntax.all.* -import cats.effect.* -``` - -Next, tracers and log handlers are provided. These are used to log applications. Tracers are used to record application traces. The log handler is used to log the application. - -The following code provides tracers and log handlers but does nothing with the entities. - -```scala 3 -given Tracer[IO] = Tracer.noop[IO] -given LogHandler[IO] = LogHandler.noop[IO] -``` - -The most common type handled by the ldbc high-level API is of the form `Executor[F, A]`, which specifies a calculation to be performed in a context where `{java | ldbc}.sql.Connection` is available, ultimately producing a value of type A. - -Let's start with an Executor program that only returns constants. - -```scala -val program: Executor[IO, Int] = Executor.pure[IO, Int](1) -``` - -Next, create a connector to connect to the database. A connector is a resource for managing connections to a database. A connector provides resources to initiate a connection to the database, execute a query, and close the connection. - -Here, ldbc uses a connector created by ldbc on its own. How to select and create a connector will be explained later. - -```scala -def connection = Connection[IO]( - host = "127.0.0.1", - port = 13306, - user = "ldbc", - password = Some("password"), - ssl = SSL.Trusted -) -``` - -Executor is a data type that knows how to connect to a database, how to pass connections, and how to clean up connections, and this knowledge allows Executor to be converted to IO to obtain an executable program. Specifically, execution yields an IO that connects to the database and executes a single transaction. - -```scala -connection - .use { conn => - program.readOnly(conn).map(println(_)) - } - .unsafeRunSync() -``` - -Hooray! I was able to calculate the constants. This is not very interesting, since we will not be asking the database to do the work, but the first step is complete. - -> Remember that all the code in this book is pure except for the call to IO.unsafeRunSync. IO.unsafeRunSync is an “end of the world” operation that usually appears only at the entry point of an application. The REPL forces the calculation to to use this to make it “happen.” - -**Execute with Scala CLI** - -This program can also be run using the Scala CLI. The following command will execute this program. - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` diff --git a/docs/src/main/mdoc/en/tutorial/Simple-Program.md b/docs/src/main/mdoc/en/tutorial/Simple-Program.md deleted file mode 100644 index 027fd52ed..000000000 --- a/docs/src/main/mdoc/en/tutorial/Simple-Program.md +++ /dev/null @@ -1,106 +0,0 @@ -{% - laika.title = Simple Program - laika.metadata.language = en -%} - -# Simple Program - -In this section, we first explain the basic usage of ldbc by creating and executing a simple program. - -※ The program environment used here is assumed to be the one built in the setup. - -## The first program - -In this program, we will create a program that connects to a database and retrieves the results of a calculation. - -Now, let's create a query that asks the database to calculate a constant using `sql string interpolator`. - -```scala -val program: Executor[IO, Option[Int]] = sql"SELECT 2".query[Int].to[Option] -``` - -Queries created with `sql string interpolator` use the `query` method to determine the type to retrieve. Here, `query[Int]` is used to get the `Int` type. Also, the `to` method determines the type to retrieve. Here, `to[Option]` is used to get the `Option` type. - -| Method | Return Type | Notes | -|--------------|----------------|-----------------------------------------------------| -| `to[List]` | `F[List[A]]` | `View all results in a list` | -| `to[Option]` | `F[Option[A]]` | `Result is 0 or 1, otherwise an error is generated` | -| `unsafe` | `F[A]` | `Exactly one result, otherwise an error will occur` | - -Finally, write a program that connects to the database and returns a value. This program connects to the database, executes the query, and retrieves the results. - -```scala -connection - .use { conn => - program.readOnly(conn).map(println(_)) - } - .unsafeRunSync() -``` - -Connected to database to calculate constants. Quite impressive. - -**Execute with Scala CLI** - -This program can also be run using the Scala CLI. The following command will execute this program. - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## Second program - -What if you want to do multiple things in one transaction? Easy! Since Executor is a monad, you can use for comprehensions to make two small programs into one big program. - -```scala 3 -val program: Executor[IO, (List[Int], Option[Int], Int)] = - for - result1 <- sql"SELECT 1".query[Int].to[List] - result2 <- sql"SELECT 2".query[Int].to[Option] - result3 <- sql"SELECT 3".query[Int].unsafe - yield (result1, result2, result3) -``` - -Finally, write a program that connects to the database and returns a value. This program connects to the database, executes the query, and retrieves the results. - -```scala -connection - .use { conn => - program.readOnly(conn).map(println(_)) - } - .unsafeRunSync() -``` - -**Run with Scala CLI**. - -This program can also be run using the Scala CLI. The following command will execute this program. - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## Third program - -Let's write a program that writes to a database. Here, we connect to the database, execute a query, and insert data. - -```scala -val program: Executor[IO, Int] = - sql"INSERT INTO user (name, email) VALUES ('Carol', 'carol@example.com')".update -``` - -The difference from the previous step is that the `commit` method is called. This commits the transaction and inserts the data into the database. - -```scala -connection - .use { conn => - program.commit(conn).map(println(_)) - } - .unsafeRunSync() -``` - -**Execute with Scala CLI**. - -This program can also be run using the Scala CLI. The following command will execute this program. - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` diff --git a/docs/src/main/mdoc/en/tutorial/Updating-Data.md b/docs/src/main/mdoc/en/tutorial/Updating-Data.md deleted file mode 100644 index 4050b6b3b..000000000 --- a/docs/src/main/mdoc/en/tutorial/Updating-Data.md +++ /dev/null @@ -1,115 +0,0 @@ -{% - laika.title = Data Update - laika.metadata.language = en -%} - -# Data Update - -This chapter describes operations to modify data in the database and how to retrieve the updated results. - -## Insert - -Insert is simple and works just like select. Here we define a method to create an `Executor` that will insert rows into the `user` table. - -```scala -def insertUser(name: String, email: String): Executor[IO, Int] = - sql"INSERT INTO user (name, email) VALUES ($name, $email)" - .update -``` - -Let's insert a line. - -```scala -insertUser("dave", "dave@example.com").commit.unsafeRunSync() -``` - -And then read it back. - -```scala -sql"SELECT * FROM user" - .query[(Int, String, String)] // Query[IO, (Int, String, String)] - .to[List] // Executor[IO, List[(Int, String, String)]] - .readOnly(conn) // IO[List[(Int, String, String)]] - .unsafeRunSync() // List[(Int, String, String)] - .foreach(println) // Unit -``` - -## Update - -The same pattern applies to updates. Here we update the user's email address. - -```scala -def updateUserEmail(id: Int, email: String): Executor[IO, Int] = - sql"UPDATE user SET email = $email WHERE id = $id" - .update -``` - -Getting Results - -```scala -updateUserEmail(1, "alice+1@example.com").commit.unsafeRunSync() - -sql"SELECT * FROM user WHERE id = 1" - .query[(Int, String, String)] // Query[IO, (Int, String, String)] - .to[Option] // Executor[IO, List[(Int, String, String)]] - .readOnly(conn) // IO[List[(Int, String, String)]] - .unsafeRunSync() // List[(Int, String, String)] - .foreach(println) // Unit -// Some((1,alice,alice+1@example.com)) -``` - -## Auto-generated keys - -When inserting, we want to return the newly generated key. We do this the hard way, first by inserting and getting the last generated key with `LAST_INSERT_ID` and then selecting the specified row. - -```scala 3 -def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = - for - _ <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".update - id <- sql"SELECT LAST_INSERT_ID()".query[Int].unsafe - task <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] - yield task -``` - -```scala -insertUser("eve", "eve@example.com").commit.unsafeRunSync() -``` - -This is frustrating, but supported by all databases (although the “get last used ID” feature varies from vendor to vendor). - -In MySQL, only rows with `AUTO_INCREMENT` set can be returned on insert. The above operation can be reduced to two statements - -If you are inserting rows using an auto-generated key, you can use the `returning` method to retrieve the auto-generated key. - -```scala 3 -def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = - for - id <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".returning[Int] - user <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] - yield user -``` - -```scala -insertUser("frank", "frank@example.com").commit.unsafeRunSync() -``` - -## Batch update - -To perform batch updates, define an `insertManyUser` method that inserts multiple rows using `NonEmptyList`. - -```scala -def insertManyUser(users: NonEmptyList[(String, String)]): Executor[IO, Int] = - val value = users.map { case (name, email) => sql"($name, $email)" } - (sql"INSERT INTO user (name, email) VALUES" ++ values(value)).update -``` - -Running this program gives the updated row count. - -```scala -val users = NonEmptyList.of( - ("greg", "greg@example.com"), - ("henry", "henry@example.com") -) - -insertManyUser(users).commit.unsafeRunSync() -``` diff --git a/docs/src/main/mdoc/en/tutorial/directory.conf b/docs/src/main/mdoc/en/tutorial/directory.conf deleted file mode 100644 index a95cc9a92..000000000 --- a/docs/src/main/mdoc/en/tutorial/directory.conf +++ /dev/null @@ -1,17 +0,0 @@ -laika.title = Tutorial -laika.navigationOrder = [ - index.md, - Setup.md, - Connection.md, - Simple-Program.md, - Database-Operations.md, - Parameterized-Queries.md, - Selecting-Data.md, - Updating-Data.md, - Error-Handling.md, - Logging.md, - Custom-Data-Type.md, - Query-Builder.md, - Schema.md, - Schema-Code-Generation.md -] diff --git a/docs/src/main/mdoc/en/tutorial/index.md b/docs/src/main/mdoc/en/tutorial/index.md deleted file mode 100644 index 02a78d0ae..000000000 --- a/docs/src/main/mdoc/en/tutorial/index.md +++ /dev/null @@ -1,15 +0,0 @@ -{% - laika.title = Intro - laika.metadata.language = en -%} - - -# Tutorial - -This section contains instructions and examples to get you started. It's written in tutorial style, intended to be read start to finish. - -## Table of Contents - -@:navigationTree { - entries = [ { target = "/en/tutorial", depth = 2 } ] -} From 01a06bf1a9da9cf4425402560748261afd03482a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:37:25 +0900 Subject: [PATCH 151/160] Migrate en document --- docs/src/main/mdoc/en/01-Table-Definitions.md | 411 +++++++++++++++ docs/src/main/mdoc/en/02-Custom-Data-Type.md | 83 +++ .../mdoc/en/03-Type-safe-Query-Builder.md | 489 ++++++++++++++++++ .../main/mdoc/en/04-Database-Connection.md | 411 +++++++++++++++ docs/src/main/mdoc/en/05-Plain-SQL-Queries.md | 39 ++ .../06-Generating-SchemaSPY-Documentation.md | 79 +++ .../main/mdoc/en/07-Schema-Code-Generation.md | 205 ++++++++ docs/src/main/mdoc/en/08-Performance.md | 38 ++ docs/src/main/mdoc/en/directory.conf | 11 +- docs/src/main/mdoc/en/index.md | 100 ++-- .../main/mdoc/ja/07-Schema-Code-Generation.md | 18 +- 11 files changed, 1842 insertions(+), 42 deletions(-) create mode 100644 docs/src/main/mdoc/en/01-Table-Definitions.md create mode 100644 docs/src/main/mdoc/en/02-Custom-Data-Type.md create mode 100644 docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md create mode 100644 docs/src/main/mdoc/en/04-Database-Connection.md create mode 100644 docs/src/main/mdoc/en/05-Plain-SQL-Queries.md create mode 100644 docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md create mode 100644 docs/src/main/mdoc/en/07-Schema-Code-Generation.md create mode 100644 docs/src/main/mdoc/en/08-Performance.md diff --git a/docs/src/main/mdoc/en/01-Table-Definitions.md b/docs/src/main/mdoc/en/01-Table-Definitions.md new file mode 100644 index 000000000..788f167a1 --- /dev/null +++ b/docs/src/main/mdoc/en/01-Table-Definitions.md @@ -0,0 +1,411 @@ +{% +laika.title = Table Definitions +laika.metadata.language = en +%} + +# Table Definitions + +This chapter describes how to work with database schemas in Scala code, especially how to manually write a schema, which is useful when starting to write an application without an existing database. If you already have a schema in your database, you can skip this step using the [code generator](/en/07-Schema-Code-Generation.md). + +The following code example assumes the following import + +```scala 3 +import ldbc.core.* +import ldbc.core.attribute.* +``` + +LDBC maintains a one-to-one mapping between Scala models and database table definitions. The mapping between the properties held by the model and the columns held by the table is done in the order of definition. Table definitions are very similar to the structure of a Create statement. This makes the construction of table definitions intuitive for the user. + +LDBC uses this table definition for a variety of purposes Generating type-safe queries, generating documents, etc. + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( // CREATE TABLE `user` ( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, + column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL +) // ); +``` + +All columns are defined by the column method. Each column has a column name, data type, and attributes. The following primitive types are supported by default and are ready to use + +- Numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigDecimal`, `BigInt` +- LOB types: `java.sql.Blob`, `java.sql.Clob`, `Array[Byte]` +- Date types: `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp` +- String +- Boolean +- java.time.* + +Nullable columns are represented by `Option[T]`, where T is one of the supported primitive types; note that any column that is not of type Option is Not Null. + +## Data Type + +The mapping of the Scala type of a property that a model has to the data type that a column has requires that the defined data type supports the Scala type. Attempting to assign an unsupported type will result in a compile error. + +The following table shows the Scala types supported by the data types. + +| Data Type | Scala Type | +|--------------|-------------------------------------------------------------------------------------------------| +| `BIT` | `Byte, Short, Int, Long` | +| `TINYINT` | `Byte, Short` | +| `SMALLINT` | `Short, Int` | +| `MEDIUMINT` | `Int` | +| `INT` | `Int, Long` | +| `BIGINT` | `Long, BigInt` | +| `DECIMAL` | `BigDecimal` | +| `FLOAT` | `Float` | +| `DOUBLE` | `Double` | +| `CHAR` | `String` | +| `VARCHAR` | `String` | +| `BINARY` | `Array[Byte]` | +| `VARBINARY` | `Array[Byte]` | +| `TINYBLOB` | `Array[Byte]` | +| `BLOB` | `Array[Byte]` | +| `MEDIUMBLOB` | `Array[Byte]` | +| `LONGBLOB` | `Array[Byte]` | +| `TINYTEXT` | `String` | +| `TEXT` | `String` | +| `MEDIUMTEXT` | `String` | +| `DATE` | `java.time.LocalDate` | +| `DATETIME` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime` | +| `TIMESTAMP` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime` | +| `TIME` | `java.time.LocalTime` | +| `YEAR` | `java.time.Instant, java.time.LocalDate, java.time.Year` | +| `BOOLEA` | `Boolean` | + +**Points to keep in mind when dealing with integer types** + +It should be noted that the range of data that can be handled, depending on whether it is signed or unsigned, does not fit within the Scala types. + +| Data Type | Signed Range | Unsigned Range | Scala Type | Range | +|-------------|----------------------------------------------|----------------------------|------------------|----------------------------------------------------------------------| +| `TINYINT` | `-128 ~ 127` | `0 ~ 255` | `Byte
Short` | `-128 ~ 127
-32768~32767` | +| `SMALLINT` | `-32768 ~ 32767` | `0 ~ 65535` | `Short
Int` | `-32768~32767
-2147483648~2147483647` | +| `MEDIUMINT` | `-8388608 ~ 8388607` | `0 ~ 16777215` | `Int` | `-2147483648~2147483647` | +| `INT` | `-2147483648 ~ 2147483647` | `0 ~ 4294967295` | `Int
Long` | `-2147483648~2147483647
-9223372036854775808~9223372036854775807` | +| `BIGINT` | `-9223372036854775808 ~ 9223372036854775807` | `0 ~ 18446744073709551615` | `Long
BigInt` | `-9223372036854775808~9223372036854775807
...` | + +To work with user-defined proprietary or unsupported types, see [Custom Types](/en/02-Custom-Data-Type.md). + +## Attribute + +Various attributes can be assigned to columns. + +- `AUTO_INCREMENT` + Mark columns as auto-increment keys when creating DDL statements and documenting SchemaSPY. + MySQL cannot return columns that are not AutoInc when inserting data. Therefore, if necessary, LDBC will check to see if the return column is properly marked as AutoInc. +- `PRIMARY_KEY` + Mark columns as primary keys when creating DDL statements and SchemaSPY documents. +- `UNIQUE_KEY` + Mark columns as unique keys when creating DDL statements and SchemaSPY documents. +- `COMMENT` + Set comments on columns when creating DDL statements and SchemaSPY documents. + +## Key Settings + +MySQL allows you to set various keys for tables, such as Unique keys, Index keys, foreign keys, etc. Let's look at how to set these keys in a table definition built with LDBC. + +### PRIMARY KEY + +A primary key is an item that uniquely identifies data in MySQL. When a primary key constraint is set on a column, the column can only contain values that do not duplicate the values of other data. It also cannot contain NULLs. As a result, only one piece of data in the table can be identified by searching for a value in a column with a primary key constraint. + +LDBC allows this primary key constraint to be set in two ways. + +1. set as an attribute of column method +2. set by keySet method of table + +**Set as an attribute of the column method** + +It is very easy to set a column method as an attribute by simply passing `PRIMARY_KEY` as the third or later argument of the column method. This allows you to set the `id` column as the primary key in the following cases + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY) +``` + +**Set by keySet method of table** + +LDBC table definitions have a method called `keySet`, where you can set a column as a primary key by passing `PRIMARY_KEY` as the column to be set as the primary key. + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => PRIMARY_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`) +// ) +``` + +The `PRIMARY_KEY` method accepts the following parameters in addition to columns + +- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH +- `Index Option` ldbc.core.Index.IndexOption + +#### Compound key (primary key) + +Not only one column, but also multiple columns can be set as a combined primary key. You can set multiple columns as primary keys by simply passing multiple columns to `PRIMARY_KEY` as primary keys. + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => PRIMARY_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// PRIMARY KEY (`id`, `name`) +// ) +``` + +Compound keys can only be set with `PRIMARY_KEY` in the `keySet` method. If multiple keys are set as attributes of the column method as shown below, each will be set as a primary key, not as a compound key. + +LDBC does not allow multiple `PRIMARY_KEY`s in a table definition to cause a compile error. However, if the table definition is used in query generation, document generation, etc., an error will occur. This is due to the restriction that only one PRIMARY KEY can be set per table. + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255), PRIMARY_KEY), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + +// CREATE TABLE `user` ( +// `id` BIGINT AUTO_INCREMENT PRIMARY KEY, +// ) +``` + +### UNIQUE KEY + +A unique key is an item that uniquely identifies data in MySQL. When a column has a uniqueness constraint, it can only contain values that do not duplicate the values of other data. + +LDBC allows this uniqueness constraint to be set in two ways. + +1. set as an attribute of column method +2. set by keySet method of table + +**Set as an attribute of the column method** + +It is very easy to set a column method as an attribute by simply passing `UNIQUE_KEY` as the third or later argument of the column method. This allows you to set the `id` column as a unique key in the following cases + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, UNIQUE_KEY) +``` + +**Set by keySet method of table** + +LDBC table definitions have a method called `keySet`, where you can set a column as a unique key by passing the column you want to set as a unique key to `UNIQUE_KEY`. + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => UNIQUE_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`) +// ) +``` + +The `UNIQUE_KEY` method accepts the following parameters in addition to columns + +- `Index Name` String +- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH +- `Index Option` ldbc.core.Index.IndexOption + +#### Compound key (unique key) + +You can set not only one column but also multiple columns as a unique key as a combined unique key. You can set multiple columns as unique keys by simply passing `UNIQUE_KEY` with the columns you want to set as unique keys. + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => UNIQUE_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// UNIQUE KEY (`id`, `name`) +// ) +``` + +Compound keys can only be set with `UNIQUE_KEY` in the `keySet` method. If you set multiple keys as attributes in the column method, each will be set as a unique key, not as a compound key. + +### INDEX KEY + +An index key is an "index" in MySQL to efficiently retrieve the desired record. + +LDBC allows this index to be set in two ways. + +1. set as an attribute of column method +2. set by keySet method of table + +**Set as an attribute of the column method** + +It is very easy to set a column method as an attribute, just pass `INDEX_KEY` as the third argument or later of the column method. This allows you to set the `id` column as an index in the following cases + +```scala 3 +column("id", BIGINT, AUTO_INCREMENT, INDEX_KEY) +``` + +**Set by keySet method of table** + +LDBC table definitions have a method called `keySet`, where you can set a column as an index key by passing the column you want to set as an index to `INDEX_KEY`. + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => INDEX_KEY(table.id)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`) +// ) +``` + +The `INDEX_KEY` method accepts the following parameters in addition to columns + +- `Index Name` String +- `Index Type` ldbc.core.Index.Type.BTREE or ldbc.core.Index.Type.HASH +- `Index Option` ldbc.core.Index.IndexOption + +#### Compound key (index key) + +You can set not only one column but also multiple columns as index keys as a combined index key. You can set up a composite index by simply passing multiple columns as index keys to `INDEX_KEY`. + +```scala 3 +val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) +) + .keySet(table => INDEX_KEY(table.id, table.name)) + +// CREATE TABLE `user` ( +// ..., +// INDEX KEY (`id`, `name`) +// ) +``` + +Compound keys can only be set with `INDEX_KEY` in the `keySet` method. If you set multiple columns as attributes of the `column` method, they will each be set as an index key, not as a composite index. + +### FOREIGN KEY + +A foreign key is a data integrity constraint (referential integrity constraint) in MySQL. A column set to a foreign key can only have values that exist in the columns of the referenced table. + +In LDBC, this foreign key constraint can be set by using the keySet method of table. + +```scala 3 +val post = Table[Post]("post")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)) +) + +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// ) +``` + +The `FOREIGN_KEY` method accepts the following parameters in addition to column and reference values. + +- `Index Name` String + +Foreign key constraints can be used to set the behavior of the parent table on delete and update. The `REFERENCE` method provides the `onDelete` and `onUpdate` methods, which can be used to set the respective behavior. + +Values that can be set can be obtained from `ldbc.core.Reference.ReferenceOption`. + +```scala 3 +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id).onDelete(Reference.ReferenceOption.RESTRICT))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT +// ) +``` + +The values that can be set are as follows + +- `RESTRICT`: Deny delete or update operations on parent tables. +- `CASCADE`: Deletes or updates rows from the parent table and automatically deletes or updates matching rows in the child tables. +- `SET_NULL`: Deletes or updates rows from the parent table and sets foreign key columns in the child table to NULL. +- `NO_ACTION`: Standard SQL keywords. In MySQL, equivalent to RESTRICT. +- `SET_DEFAULT`: This action is recognized by the MySQL parser, but both InnoDB and NDB will reject table definitions containing an ON DELETE SET DEFAULT or ON UPDATE SET DEFAULT clause. + +#### Compound key (foreign key) + +Not only one column, but also multiple columns can be combined as a foreign key. Simply pass multiple columns to `FOREIGN_KEY` to be set as foreign keys as a compound foreign key. + +```scala 3 +val post = Table[Post]("post")( + column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("category", SMALLINT[Short]) +) + +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]), + column("post_category", SMALLINT[Short]) +) + .keySet(table => FOREIGN_KEY((table.postId, table.postCategory), REFERENCE(post, (post.id, post.category)))) + +// CREATE TABLE `user` ( +// ..., +// FOREIGN KEY (`post_id`, `post_category`) REFERENCES `post` (`id`, `category`) +// ) +``` + +### Constraint Name + +MySQL allows you to give arbitrary names to constraints by using CONSTRAINT. The constraint name must be unique on a per-database basis. + +LDBC provides the CONSTRAINT method, so the process of setting constraints such as key constraints can be set by simply passing the process to the CONSTRAINT method. + +```scala 3 +val user = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), + column("post_id", BIGINT[Long]) +) + .keySet(table => CONSTRAINT("fk_post_id", FOREIGN_KEY(table.postId, REFERENCE(post, post.id)))) + +// CREATE TABLE `user` ( +// ..., +// CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// ) +``` diff --git a/docs/src/main/mdoc/en/02-Custom-Data-Type.md b/docs/src/main/mdoc/en/02-Custom-Data-Type.md new file mode 100644 index 000000000..e2659c32c --- /dev/null +++ b/docs/src/main/mdoc/en/02-Custom-Data-Type.md @@ -0,0 +1,83 @@ +{% +laika.title = Custom Data Type +laika.metadata.language = en +%} + +# Custom Data Type + +This chapter describes how to use user-specific or unsupported types in table definitions built with LDBC. + +The following code example assumes the following import + +```scala 3 +import ldbc.core.* +``` + +The way to use user-specific or unsupported types is to tell the column data type what type to treat as a data type; DataType provides a `mapping` method that can be used to set this up as an implicit type conversion. + +```scala 3 +case class User( + id: Long, + name: User.Name, + age: Option[Int], +) + +object User: + + case class Name(firstName: String, lastName: String) + + given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` + +LDBC does not allow multiple columns to be merged into a single property of a model, since the purpose of LDBC is to provide a one-to-one mapping between models and tables, and to construct database table definitions in a type-safe manner. + +Therefore, it is not allowed to have different number of properties in the table definition and in the model. The following implementation will result in a compile error + +```scala 3 +case class User( + id: Long, + name: User.Name, + age: Option[Int], +) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("first_name", VARCHAR(255)), + column("last_name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` + +If you wish to implement the above, please consider the following implementation. + +```scala 3 +case class User( + id: Long, + firstName: String, + lastName: String, + age: Option[Int], +): + + val name: User.Name = User.Name(firstName, lastName) + +object User: + + case class Name(firstName: String, lastName: String) + + val table = Table[User]("user")( + column("id", BIGINT[Long], AUTO_INCREMENT), + column("first_name", VARCHAR(255)), + column("last_name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)) + ) +``` diff --git a/docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md b/docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md new file mode 100644 index 000000000..b25d340cc --- /dev/null +++ b/docs/src/main/mdoc/en/03-Type-safe-Query-Builder.md @@ -0,0 +1,489 @@ +{% +laika.title = Type-safe Query Construction +laika.metadata.language = en +%} + +# Type-safe Query Construction + +This chapter describes how to use LDBC-built table definitions to construct type-safe queries. + +The following dependencies must be set up for the project + +```scala +libraryDependencies += "@ORGANIZATION@" %% "ldbc-query-builder" % "@VERSION@" +``` + +If you have not yet read how to define tables in LDBC, we recommend that you read the chapter [Table Definitions](/en/01-Table-Definitions.md) first. + +The following code example assumes the following import + +```scala 3 +import cats.effect.IO +import ldbc.core.* +import ldbc.query.builder.TableQuery +``` + +LDBC performs type-safe query construction by passing table definitions to TableQuery. + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), +) + +val userQuery = TableQuery[User](table) +``` + +## SELECT + +A type-safe way to construct a SELECT statement is to use the `select` method provided by TableQuery, which is implemented in LDBC to mimic a plain query, making query construction intuitive. LDBC is also designed so that you can see at a glance what kind of query is being constructed. + +To construct a SELECT statement that retrieves only specific columns, simply specify the columns you want to retrieve in the `select` method. + +```scala 3 +val select = userQuery.select(_.id) + +select.statement === "SELECT `id` FROM user" +``` + +To specify multiple columns, simply specify the columns you wish to retrieve using the `select` method and return a tuple of the specified columns. + +```scala 3 +val select = userQuery.select(user => (user.id, user.name)) + +select.statement === "SELECT `id`, `name` FROM user" +``` + +If you want to specify all columns, you can construct it by using the `selectAll` method provided by TableQuery. + +```scala 3 +val select = userQuery.selectAll + +select.statement === "SELECT `id`, `name`, `age` FROM user" +``` + +If you want to get the number of a specific column, you can construct it by using `count` on the specified column. + +```scala 3 +val select = userQuery.select(_.id.count) + +select.statement === "SELECT COUNT(id) FROM user" +``` + +### WHERE + +A type-safe way to set a Where condition in a query is to use the `where` method. + +```scala 3 +val select = userQuery.select(_.id).where(_.name === "Test") + +select.statement === "SELECT `id` FROM user WHERE name = ?" +``` + +The following is a list of conditions that can be used in the `where` method. + +| condition | statement | +|----------------------------------------|---------------------------------------| +| `===` | `column = ?` | +| `>=` | `column >= ?` | +| `>` | `column > ?` | +| `<=` | `column <= ?` | +| `<` | `column < ?` | +| `<>` | `column <> ?` | +| `!==` | `column != ?` | +| `IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL")` | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | +| `<=>` | `column <=> ?` | +| `IN (value, value, ...)` | `column IN (?, ?, ...)` | +| `BETWEEN (start, end)` | `column BETWEEN ? AND ?` | +| `LIKE (value)` | `column LIKE ?` | +| `LIKE_ESCAPE (like, escape)` | `column LIKE ? ESCAPE ?` | +| `REGEXP (value)` | `column REGEXP ?` | +| `<<` (value) | `column << ?` | +| `>>` (value) | `column >> ?` | +| `DIV (cond, result)` | `column DIV ? = ?` | +| `MOD (cond, result)` | `column MOD ? = ?` | +| `^ (value)` | `column ^ ?` | +| `~ (value)` | `~column = ?` | + +### GROUP BY/Having + +A type-safe way to set a Group By clause in a query is to use the `groupBy` method. + +Using `groupBy` allows you to group data based on the value of a column name you specify when retrieving data with `select`. + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3) + +select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age" +``` + +When grouping, the number of data that can be retrieved with `select` is the number of groups. So, when grouping, you can retrieve the values of the columns specified for grouping, or the results of aggregating the column values by group using the provided functions. + +The `having` allows you to set the conditions for retrieval with respect to data grouped and retrieved by `groupBy`. + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).groupBy(_._3).having(_._3 > 20) + +select.statement === "SELECT `id`, `name`, `age` FROM user GROUP BY age HAVING age > ?" +``` + +### ORDER BY + +A type-safe way to set an ORDER BY clause in a query is to use the `orderBy` method. + +Using `orderBy` allows you to get the results sorted by the values of the columns you specify when retrieving data with `select`. + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age) + +select.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age" +``` + +If you want to specify ascending/descending order, simply call `asc`/`desc` for the columns, respectively. + +```scala 3 +val desc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.desc) + +desc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age DESC" + +val asc = userQuery.select(user => (user.id, user.name, user.age)).orderBy(_.age.asc) + +asc.statement === "SELECT `id`, `name`, `age` FROM user ORDER BY age ASC" +``` + +### LIMIT/OFFSET + +A type-safe way to set the LIMIT and OFFSET clauses in a query is to use the `limit`/`offset` methods. + +The `limit` can be set to the maximum number of rows of data to retrieve when `select` is executed, and the `offset` can be set to the number of rows of data to retrieve. + +```scala 3 +val select = userQuery.select(user => (user.id, user.name, user.age)).limit(100).offset(50) + +select.statement === "SELECT `id`, `name`, `age` FROM user LIMIT ? OFFSET ?" +``` + +## JOIN/LEFT JOIN/RIGHT JOIN + +A type-safe way to set a Join on a query is to use the `join`/`leftJoin`/`rightJoin` methods. + +The following definition is used as a sample for Join. + +```scala 3 +case class Country(code: String, name: String) +object Country: + val table = Table[Country]("country")( + column("code", CHAR(3), PRIMARY_KEY), + column("name", VARCHAR(255)) + ) + +case class City(id: Long, name: String, countryCode: String) +object City: + val table = Table[City]("city")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("country_code", CHAR(3)) + ) + +case class CountryLanguage( + countryCode: String, + language: String +) +object CountryLanguage: + val table: Table[CountryLanguage] = Table[CountryLanguage]("country_language")( + column("country_code", CHAR(3)), + column("language", CHAR(30)) + ) + +val countryQuery = TableQuery[Country](Country.table) +val cityQuery = TableQuery[City](City.table) +val countryLanguageQuery = TableQuery[CountryLanguage](CountryLanguage.table) +``` + +If you want to do a simple Join first, use `join`. +The first argument of `join` is the table to be joined, and the second argument is a function that compares the source table with the columns of the table to be joined. This corresponds to the ON clause in Join. + +After the join, the `select` will specify columns from the two tables. + +```scala 3 +val join = countryQuery.join(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country JOIN city ON country.code = city.country_code" +``` + +Next, if you want to perform a Left Join, which is a left outer join, use `leftJoin`. +The implementation itself is the same as for a simple Join, only `join` is changed to `leftJoin`. + +```scala 3 +val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city ON country.code = city.country_code" +``` + +The difference from a simple Join is that when using `leftJoin`, the records retrieved from the table to be joined may be NULL. + +Therefore, in LDBC, all records in the column retrieved from the table passed to `leftJoin` will be of type Option. + +```scala 3 +val leftJoin = countryQuery.leftJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) // (String, Option[String]) +``` + +Next, if you want to perform a Right Join, which is a right outer join, use `rightJoin`. +The implementation itself is the same as that of simple Join, only `join` is changed to `rightJoin`. + +```scala 3 +val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) + +join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN city ON country.code = city.country_code" +``` + +The difference from a simple Join is that when using `rightJoin`, the records retrieved from the join source table may be NULL. + +Therefore, in LDBC, all records of a column retrieved from a join source table using `rightJoin` are of type Option. + +```scala 3 +val rightJoin = countryQuery.rightJoin(cityQuery)((country, city) => country.code === city.countryCode) + .select((country, city) => (country.name, city.name)) // (Option[String], String) +``` + +If multiple joins are desired, this can be accomplished by calling any Join method in the method chain. + +```scala 3 +val join = + (countryQuery join cityQuery)((country, city) => country.code === city.countryCode) + .rightJoin(countryLanguageQuery)((_, city, countryLanguage) => city.countryCode === countryLanguage.countryCode) + .select((country, city, countryLanguage) => (country.name, city.name, countryLanguage.language)) // (Option[String], Option[String], String)] + +join.statement = + """ + |SELECT + | country.`name`, + | city.`name`, + | country_language.`language` + |FROM country + |JOIN city ON country.code = city.country_code + |RIGHT JOIN country_language ON city.country_code = country_language.country_code + |""".stripMargin +``` + +Note that a `rightJoin` join with multiple joins will result in NULL-acceptable access to all records retrieved from the previously joined table, regardless of what the previous join was. + +## Custom Data Type + +In the previous section, we used the `mapping` method of DataType to map custom types to DataType in order to use user-specific or unsupported types. ([reference](/en/02-Custom-Data-Type.md)) + +LDBC separates the table definition from the process of connecting to the database. +Therefore, if you want to retrieve data from the database and convert it to a user-specific or unsupported type, you must link the method of retrieving data from the ResultSet to the user-specific or unsupported type. + +For example, if you want to map a user-defined Enum to a string type + +```scala 3 +enum Custom: + case ... + +given ResultSetReader[IO, Custom] = + ResultSetReader.mapping[IO, str, Custom](str => Custom.valueOf(str)) +``` + +※ This process may be integrated with DataType mapping in a future version. + +## INSERT + +A type-safe way to construct an INSERT statement is to use the following methods provided by TableQuery. + +- insert +- insertInto +- += +- ++= + +**insert** + +The `insert` method is passed a tuple of data to insert. The tuples must have the same number and type of properties as the model. Also, the order of the inserted data must be in the same order as the model properties and table columns. + +```scala 3 +val insert = userQuery.insert((1L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" +``` + +If you want to insert multiple data, you can construct it by passing multiple tuples to the `insert` method. + +```scala 3 +val insert = userQuery.insert((1L, "name", None), (2L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +**insertInto** + +The `insert` method inserts data into all columns the table has, but if you want to insert data only into specific columns, use the `insertInto` method. + +This can be used, for example, to exclude data insertion into columns with AutoIncrement or Default values. + +```scala 3 +val insert = userQuery.insertInto(user => (user.name, user.age)).values(("name", None)) + +insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?)" +``` + +If you want to insert multiple data, you can construct it by passing an array of tuples to `values`. + +```scala 3 +val insert = userQuery.insertInto(user => (user.name, user.age)).values(List(("name", None), ("name", Some(20)))) + +insert.statement === "INSERT INTO user (`name`, `age`) VALUES(?, ?), (?, ?)" +``` + +**+=** + +The `+=` method can be used to construct an INSERT statement using a model. Note that when using a model, data is inserted into all columns. + +```scala 3 +val insert = userQuery += User(1L, "name", None) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?)" +``` + +**++=** + +Use the `++=` method if you want to insert multiple data using the model. + +```scala 3 +val insert = userQuery ++= List(User(1L, "name", None), User(2L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" +``` + +### ON DUPLICATE KEY UPDATE + +Inserting a row with an ON DUPLICATE KEY UPDATE clause will cause an UPDATE of the old row if the UNIQUE index or PRIMARY KEY has duplicate values. + +There are two ways to achieve this in LDBC: using `insertOrUpdate{s}` or using `onDuplicateKeyUpdate` for `Insert`. + +```scala 3 +val insert = userQuery.insertOrUpdate((1L, "name", None)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `id` = new_user.`id`, `name` = new_user.`name`, `age` = new_user.`age`" +``` + +Note that if you use `insertOrUpdate{s}`, all columns will be updated. If you have duplicate values and wish to update only certain columns, use `onDuplicateKeyUpdate` to specify only the columns you wish to update. + +```scala 3 +val insert = userQuery.insert((1L, "name", None)).onDuplicateKeyUpdate(v => (v.name, v.age)) + +insert.statement === "INSERT INTO user (`id`, `name`, `age`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `age` = new_user.`age`" +``` + +## UPDATE + +A type-safe way to construct an UPDATE statement is to use the `update` method provided by TableQuery. + +The first argument of the `update` method is the name of the model property, not the column name of the table, and the second argument is the value to be updated. The type of the value passed as the second argument must be the same as the type of the property specified in the first argument. + +```scala 3 +val update = userQuery.update("name", "update name") + +update.statement === "UPDATE user SET name = ?" +``` + +If a property name that does not exist is specified as the first argument, a compile error occurs. + +```scala 3 +val update = userQuery.update("hoge", "update name") // Compile error +``` + +If you want to update multiple columns, use the `set` method. + +```scala 3 +val update = userQuery.update("name", "update name").set("age", Some(20)) + +update.statement === "UPDATE user SET name = ?, age = ?" +``` + +You can also prevent the `set` method from generating queries based on conditions. + +```scala 3 +val update = userQuery.update("name", "update name").set("age", Some(20), false) + +update.statement === "UPDATE user SET name = ?" +``` + +You can also use a model to construct the UPDATE statement. Note that if you use a model, all columns will be updated. + +```scala 3 +val update = userQuery.update(User(1L, "update name", None)) + +update.statement === "UPDATE user SET id = ?, name = ?, age = ?" +``` + +### WHERE + +The `where` method can also be used to set a where condition on the update statement. + +```scala 3 +val update = userQuery.update("name", "update name").set("age", Some(20)).where(_.id === 1) + +update.statement === "UPDATE user SET name = ?, age = ? WHERE id = ?" +``` + +See [where item](/en/03-Type-safe-Query-Builder.md) in the Insert statement for conditions that can be used in the `where` method. + +## DELETE + +A type-safe way to construct a DELETE statement is to use the `delete` method provided by TableQuery. + +```scala 3 +val delete = userQuery.delete + +delete.statement === "DELETE FROM user" +``` + +### WHERE + +The `where` method can also be used to set a Where condition on a delete statement. + +```scala 3 +val delete = userQuery.delete.where(_.id === 1) + +delete.statement === "DELETE FROM user WHERE id = ?" +``` + +See [where item](/en/03-Type-safe-Query-Builder.md) in the Insert statement for conditions that can be used in the `where` method. + +## DDL + +A type-safe way to construct DDL is to use the following methods provided by TableQuery. + +- createTable +- dropTable +- truncateTable + +If you are using spec2, you can run DDL before and after the test as follows. + +```scala 3 +import cats.effect.IO +import cats.effect.unsafe.implicits.global + +import org.specs2.mutable.Specification +import org.specs2.specification.core.Fragments +import org.specs2.specification.BeforeAfterEach + +object Test extends Specification, BeforeAfterEach: + + override def before: Fragments = + step((tableQuery.createTable.update.autoCommit(dataSource) >> IO.println("Complete create table")).unsafeRunSync()) + + override def after: Fragments = + step((tableQuery.dropTable.update.autoCommit(dataSource) >> IO.println("Complete drop table")).unsafeRunSync()) +``` diff --git a/docs/src/main/mdoc/en/04-Database-Connection.md b/docs/src/main/mdoc/en/04-Database-Connection.md new file mode 100644 index 000000000..d3f3e42e6 --- /dev/null +++ b/docs/src/main/mdoc/en/04-Database-Connection.md @@ -0,0 +1,411 @@ +{% +laika.title = Database Connection +laika.metadata.language = en +%} + +# Database Connection + +This chapter describes how to use queries built with LDBC to process connections to databases. + +The following dependencies must be set up for the project + +```scala +libraryDependencies ++= Seq( + "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", + "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" +) +``` + +If you have not yet read about how to build queries with LDBC, we recommend that you read the chapter [Building Type-Safe Queries](/en/03-Type-safe-Query-Builder.md) first. + +The following code example assumes the following import + +```scala 3 +import com.mysql.cj.jdbc.MysqlDataSource + +import cats.effect.IO +// This is just for testing. Consider using cats.effect.IOApp instead of calling +// unsafe methods directly. +import cats.effect.unsafe.implicits.global + +import ldbc.sql.* +import ldbc.dsl.io.* +import ldbc.dsl.logging.ConsoleLogHandler +import ldbc.query.builder.TableQuery +``` + +Table definitions use the following + +```scala 3 +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), +) + +val userQuery = TableQuery[User](table) +``` + +## Using DataSource + +LDBC uses JDBC's DataSource for database connections, and since LDBC does not provide an implementation for building this DataSource, it is necessary to use a library such as mysql or HikariCP. In this example, we will use mysqlDataSource to build the DataSource. + +```scala 3 +private val dataSource = new MysqlDataSource() +dataSource.setServerName("127.0.0.1") +dataSource.setPortNumber(3306) +dataSource.setDatabaseName("database name") +dataSource.setUser("user name") +dataSource.setPassword("password") +``` + +## Log + +LDBC can export execution and error logs of Database connections in any format using any logging library. + +A logger using Cats Effect's Console is provided as standard, which can be used during development. + +```scala 3 +given LogHandler[IO] = ConsoleLogHandler[IO] +``` + +### Customize + +Use `ldbc.dsl.logging.LogHandler` to customize logs using any logging library. + +The following is the standard implementation of logging: LDBC generates the following three types of events on database connections + +- Success: Successful processing +- ProcessingFailure: Error in processing after data acquisition or before database connection +- ExecFailure: Error processing connection to database + +Pattern matching is used to sort out what logs to write for each event. + +```scala 3 +def consoleLogger[F[_]: Console: Sync]: LogHandler[F] = + case LogEvent.Success(sql, args) => + Console[F].println( + s"""Successful Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) + case LogEvent.ProcessingFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed ResultSet Processing: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) + case LogEvent.ExecFailure(sql, args, failure) => + Console[F].errorln( + s"""Failed Statement Execution: + | $sql + | + | arguments = [${ args.mkString(",") }] + |""".stripMargin + ) >> Console[F].printStackTrace(failure) +``` + +## Query + +Constructing a `select` statement allows the use of the `toList`/`headOption`/`unsafe` methods. These methods are used to determine the format of the data to be retrieved. If you do not specify any particular type, the column type specified in the `select` method will be returned as a Tuple. + +### toList + +The `toList` method is used to retrieve a list of data as a result of executing a query. If you use the `toList` method to process the database and get zero data, an empty array will be returned. + +```scala 3 +val query1 = userQuery.selectAll.toList // List[(Long, String, Option[Int])] +``` + +Specifying a model in the `toList` method allows the data after acquisition to be converted to the specified model. + +```scala 3 +val query = userQuery.selectAll.toList[User] // User +``` + +The model type specified in the `toList` method must match the Tuple type specified in the `select` method or be type-convertible from the Tuple type to the specified model. + +```scala 3 +val query1 = userQuery.select(user => (user.name, user.age)).toList[User] // Compile error + +case class Test(name: String, age: Option[Int]) +val query2 = userQuery.select(user => (user.name, user.age)).toList[Test] // Test +``` + +### headOption + +If you want to get the first data as an Optional result of the query, use the `headOption` method. If the result of database processing using the `headOption` method is zero, none is returned. + +Note that if you use the `headOption` method, only the first data will be returned, even if you execute a query that retrieves multiple data. + +```scala 3 +val query1 = userQuery.selectAll.headOption // Option[(Long, String, Option[Int])] +val query2 = userQuery.selectAll.headOption[User] // Option[User] +``` + +### unsafe + +When using the `unsafe` method, it is the same as the `headOption` method in that it returns only the first case of the retrieved data, but the data is returned as is, not as Optional. If the number of data returned is zero, an exception will be raised and appropriate exception handling is required. + +It is named `unsafe` because it is likely to raise an exception at runtime. + +```scala 3 +val query1 = userQuery.selectAll.unsafe // (Long, String, Option[Int]) +val query2 = userQuery.selectAll.unsafe[User] // User +``` + +## Update + +Constructing an `insert/update/delete` statement allows you to use the `update` method. The `update` method returns the number of write operations to the database. + +```scala 3 +val insert = userQuery.insert((1L, "name", None)).update // Int +val update = userQuery.update("name", "update name").update // Int +val delete = userQuery.delete.update // Int +``` + +In the case of an `insert` statement, you may want the values generated by AutoIncrement to be returned when inserting data. In this case, use the `returning` method instead of the `update` method to specify the columns to be returned. + +```scala 3 +val insert = userQuery.insert((1L, "name", None)).returning("id") // Long +``` + +The value specified in the `returning` method must be the name of a property that the model has. Also, if the specified property does not have the AutoIncrement attribute set on the table definition, an error will occur. + +In MySQL, the only value that can be returned when inserting data is the AutoIncrement column, so the same specification applies to LDBC. + +## Perform database operations + +Before making a database connection, commit timing, read/write-only, and other settings must be made. + +### Read Only + +The `readOnly` method can be used to make the processing of a query to be executed read-only. The `readOnly` method can also be used with `insert/update/delete` statements, but it will result in an error at runtime because of the write operation. + +```scala 3 +val read = userQuery.selectAll.toList.readOnly(dataSource) +``` + +### Auto Commit + +The `autoCommit` method can be used to set the query processing to commit at each query execution. + +```scala 3 +val read = userQuery.insert((1L, "name", None)).update.autoCommit(dataSource) +``` + +### Transaction + +The `transaction` method can be used to combine multiple database connection operations into a single transaction. + +The return value of the `toList/headOption/unsafe/returning/update` method is of type `Kleisli[F, Connection[F], T]`. Therefore, you can use map or flatMap to combine the process into one. + +By using the `transaction` method on a single `Kleisli[F, Connection[F], T]`, all database connection operations performed within will be combined into a single transaction. + +```scala 3 +(for + result1 <- userQuery.insert((1L, "name", None)).returning("id") + result2 <- userQuery.update("name", "update name").update + ... +yield ...).transaction(dataSource) +``` + +## Database Action + +There is also a way to perform database processing using `Database` with connection information to the database. + +There are two ways to construct a `Database`: using the DriverManager or generating one from a DataSource. The following is an example of constructing a `Database` with connection information to a database using a MySQL driver. + +```scala 3 +val db = Database.fromMySQLDriver[IO]("database name", "host", "port number", "user name", "password") +``` + +The advantages of using `Database` to perform database processing are as follows + +- Simplifies DataSource construction (when using DriverManager) +- Eliminates the need to pass a DataSource for each query + +The method using `Database` is merely a simplified method of passing a DataSource, so there is no difference in execution results between the two. +The only difference is whether the processes are combined using `flatMap` or other methods and executed in a method chain, or whether the combined processes are executed using `Database`. Therefore, the user can choose the execution method of his/her choice. + +**Read Only** + +```scala 3 +val user: Option[User] = db.readOnly(userQuery.selectAll.headOption[User]).unsafeRunSync() +``` + +**Auto Commit** + +```scala 3 +val result = db.autoCommit(userQuery.insert((1L, "name", None)).update).unsafeRunSync() +``` + +**Transaction** + +```scala 3 +db.transaction(for + result1 <- userQuery.insert((1L, "name", None)).returning("id") + result2 <- userQuery.update("name", "update name").update + ... +yield ...).unsafeRunSync() +``` + +### Database model + +In LDBC, the `Database` model is also used for purposes other than holding database connection information. Another use is for SchemaSPY documentation generation, see [here](/ja/06-Generating-SchemaSPY-Documentation.md) for information on SchemaSPY document generation. + +If you have already generated a `Database` model for another use, you can use that model to build a `Database` with database connection information. + +```scala 3 +import ldbc.dsl.io.* + +val database: Database = ??? + +val db = database.fromDriverManager() +// or +val db = database.fromDriverManager("user name", "password") +``` + +### Use in method chain + +The `Database` model can also be used in place of `DataSource` in `TableQuery` methods. + +```scala 3 +val read = userQuery.selectAll.toList.readOnly(db) +val commit = userQuery.insert((1L, "name", None)).update.autoCommit(db) +val transaction = (for + result1 <- userQuery.insert((1L, "name", None)).returning("id") + result2 <- userQuery.update("name", "update name").update + ... +yield ...).transaction(db) +``` + +## Using a HikariCP Connection Pool + +`ldbc-hikari` provides a builder to build HikariConfig and HikariDataSource for building HikariCP connection pools. + +```scala +libraryDependencies ++= Seq( + "@ORGANIZATION@" %% "ldbc-hikari" % "@VERSION@", +) +``` + +`HikariConfigBuilder` is a builder to build `HikariConfig` of HikariCP as the name suggests. + +```scala 3 +val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.default.build() +``` + +The `HikariConfigBuilder` has a `default` and a `from` method. When `default` is used, the `HikariConfig` is constructed by retrieving the target values from the Config based on the LDBC specified path. + +```text +ldbc.hikari { + jdbc_url = ... + username = ... + password = ... +} +``` + +If you want to specify a user-specific path, you must use the `from` method and pass the path you want to retrieve as an argument. + +```scala 3 +val hikariConfig: com.zaxxer.hikari.HikariConfig = HikariConfigBuilder.from("custom.path").build() + +// custom.path { +// jdbc_url = ... +// username = ... +// password = ... +// } +``` + +Please refer to [official](https://github.com/brettwooldridge/HikariCP) for details on what can be set in HikariCP. + +The following is a list of keys that can be set for Config. + +| Key name | Description | Type | +|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------|------------| +| `catalog` | `Default catalog name to be set when connecting` | `String` | +| `connection_timeout` | `Maximum number of milliseconds the client will wait for a connection from the pool` | `Duration` | +| `idle_timeout` | `Maximum time (in milliseconds) that a connection is allowed to be idle in the pool` | `Duration` | +| `leak_detection_threshold` | `Time a connection is out of the pool before a message indicating a possible connection leak is logged` | `Duration` | +| `maximum_pool_size` | `Maximum size allowed by the pool, including both idle and in-use connections` | `Int` | +| `max_lifetime` | `Maximum lifetime of connections in the pool` | `Duration` | +| `minimum_idle` | `Minimum number of idle connections that HikariCP will try to keep in the pool, including both idle and in-use connections` | `Int` | +| `pool_name` | `Connection pool name` | `String` | +| `allow_pool_suspension` | `Whether to allow pool suspend` | `Boolean` | +| `auto_commit` | `Default autocommit behavior for connections in the pool` | `Boolean` | +| `connection_init_sql` | `SQL string to be executed when a new connection is created, before it is added to the pool` | `String` | +| `connection_test_query` | `SQL query to execute to test the validity of the connection` | `String` | +| `data_source_classname` | `Fully qualified class name of the JDBC DataSource to be used to create Connections` | `String` | +| `initialization_fail_timeout` | `Pool initialization failure timeout` | `Duration` | +| `isolate_internal_queries` | `Whether internal pool queries (mainly validity checks) are separated in their own transaction by Connection.rollback()` | `Boolean` | +| `jdbc_url` | `JDBC URL` | `String` | +| `readonly` | `Whether connections to be added to the pool should be set as read-only connections` | `Boolean` | +| `register_mbeans` | `Whether HikariCP self-registers HikariConfigMXBean and HikariPoolMXBean in JMX` | `Boolean` | +| `schema` | `Default schema name to set when connecting` | `String` | +| `username` | `Default username used for calls to DataSource.getConnection(username,password)` | `String` | +| `password` | `Default password used for calling DataSource.getConnection(username,password)` | `String` | +| `driver_class_name` | `Driver class name to be used` | `String` | +| `transaction_isolation` | `Default transaction isolation level` | `String` | + +The `HikariDataSourceBuilder` allows you to build a `HikariDataSource` for HikariCP. + +The `HikariDataSource` built by the builder is managed as a `Resource` since the connection pool is a lifetime managed object and needs to be shut down cleanly. + +```scala 3 +val dataSource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() +``` + +The `HikariDataSource` built via `buildDataSource` uses `HikariConfig`, which is built internally by retrieving settings from Config based on the LDBC specified path. +This is equivalent to `HikariConfig` generated via `default` in `HikariConfigBuilder`. + +If you want to use a user-specified `HikariConfig`, you can use `buildFromConfig` to build a `HikariDataSource`. + +```scala 3 +val hikariConfig = ??? +val dataSource = HikariDataSourceBuilder.default[IO].buildFromConfig(hikariConfig) +``` + +A `HikariDataSource` built with `HikariDataSourceBuilder` is usually executed using IOApp. + +```scala 3 +object HikariApp extends IOApp: + + val dataSourceResource: Resource[IO, HikariDataSource] = HikariDataSourceBuilder.default[IO].buildDataSource() + + def run(args: List[String]): IO[ExitCode] = + dataSourceResource.use { dataSource => + ... + } +``` + +### HikariDatabase + +There is also a way to build a `Database` with HikariCP connection information. + +The `HikariDatabase` is managed as a `Resource` like the `HikariDataSource`. +Therefore, it is usually executed using IOApp. + +```scala 3 +object HikariApp extends IOApp: + + val hikariConfig = ??? + val databaseResource: Resource[F, Database[F]] = HikariDatabase.fromHikariConfig[IO](hikariConfig) + + def run(args: List[String]): IO[ExitCode] = + databaseResource.use { database => + for + result <- database.readOnly(...) + yield ExitCode.Success + } +``` diff --git a/docs/src/main/mdoc/en/05-Plain-SQL-Queries.md b/docs/src/main/mdoc/en/05-Plain-SQL-Queries.md new file mode 100644 index 000000000..55af4c44f --- /dev/null +++ b/docs/src/main/mdoc/en/05-Plain-SQL-Queries.md @@ -0,0 +1,39 @@ +{% +laika.title = Plain SQL Queries +laika.metadata.language = en +%} + +# Plain SQL Queries + +Sometimes you may need to write your own SQL code for operations that are not well supported at a higher level of abstraction; instead of going back to the lower layers of JDBC, you can use LDBC's Plain SQL queries in the Scala-based API. +This chapter describes how to use Plain SQL queries in LDBC to process connections to databases in such cases. + +See the previous chapter on [Database Connections](/en/04-Database-Connection.md) for project dependencies and the use and logging of DataSource. + +## Plain SQL + +LDBC uses sql string interpolation with literal SQL strings to construct plain queries as follows + +Variables and expressions injected into the query are converted to bind variables in the resulting query string. Since they are not inserted directly into the query string, there is no risk of SQL injection attacks. + +```scala 3 +val select = sql"SELECT id, name, age FROM user WHERE id = $id" // SELECT id, name, age FROM user WHERE id = ? +val insert = sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)" // INSERT INTO user (id, name, age) VALUES(?, ?, ?) +val update = sql"UPDATE user SET id = $id, name = $name, age = $age" // UPDATE user SET id = ?, name = ?, age = ? +val delete = sql"DELETE FROM user WHERE id = $id" // DELETE FROM user WHERE id = ? +``` + +Plain SQL queries simply construct SQL statements at runtime. While this provides a safe and easy way to construct complex statements, it is merely an embedded string. Any syntax errors in the statement or type mismatch between the database and the Scala code cannot be detected at compile time. + +Please refer to the [Query](/en/04-Database-Connection.md#query) item in the previous section "Database Connection" for information on setting the return type of the query result and the connection method. +It is built and works the same way as a query built using a table definition. + +Plain queries and type-safe queries are constructed differently, but the implementation is the same, including the subsequent connection methods. Therefore, it is possible to combine the two and execute the query. + +```scala 3 +(for + result1 <- sql"INSERT INTO user (id, name, age) VALUES($id, $name, $age)".update + result2 <- userQuery.update("name", "update name").update + ... +yield ...).transaction +``` diff --git a/docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md b/docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md new file mode 100644 index 000000000..aa42c685d --- /dev/null +++ b/docs/src/main/mdoc/en/06-Generating-SchemaSPY-Documentation.md @@ -0,0 +1,79 @@ +{% +laika.title = SchemaSPY Document Generation +laika.metadata.language = en +%} + +# SchemaSPY Document Generation + +This chapter describes how to use table definitions built in LDBC to create SchemaSPY documents. + +The following dependencies must be set up for the project + +```scala +libraryDependencies += "@ORGANIZATION@" %% "ldbc-schemaspy" % "@VERSION@" +``` + +If you have not yet read how to define tables in LDBC, we recommend that you read the chapter [Table Definitions](/en/01-Table-Definitions.md) first. + +The following code example assumes the following import + +```scala 3 +import ldbc.core.* +import ldbc.schemaspy.SchemaSpyGenerator +``` + +## Generated from table definitions + +SchemaSPY connects to the database to obtain Meta information and table structures, and generates documents based on this information. LDBC, on the other hand, does not connect to the database, but generates SchemaSPY documents using the table structures constructed by LDBC. +Some items deviate from the documentation generated using SchemaSPY simply because it does not make a connection to the database. For example, information such as the number of records currently stored in a table cannot be displayed. + +Database information is required to generate documents, and LDBC has a trait for representing database information. + +A sample of database information built using `ldbc.core.Database` is shown below. + +```scala 3 +case class SampleLdbcDatabase( + schemaMeta: Option[String] = None, + catalog: Option[String] = Some("def"), + host: String = "127.0.0.1", + port: Int = 3306 +) extends Database: + + override val databaseType: Database.Type = Database.Type.MySQL + + override val name: String = "sample_ldbc" + + override val schema: String = "sample_ldbc" + + override val character: Option[Character] = None + + override val collate: Option[Collate] = None + + override val tables = Set( + ... // Enumerate table structures built with LDBC + ) +``` + +Use `SchemaSpyGenerator` to generate SchemaSPY documents. Pass the generated database definition to the `default` method and call `generate` to generate SchemaSPY files at the file location specified in the second argument. + +```scala 3 +@main +def run(): Unit = + val file = java.io.File("document") + SchemaSpyGenerator.default(SampleLdbcDatabase(), file).generate() +``` + +Open the generated file `index.html` to see the SchemaSPY documentation. + +## Generated from database connection + +SchemaSpyGenerator also has a `connect` method. This method connects to the database and generates documents in the same way as the standard SchemaSpy generator. + +```scala 3 +@main +def run(): Unit = + val file = java.io.File("document") + SchemaSpyGenerator.connect(SampleLdbcDatabase(), "user name", "password" file).generate() +``` + +The process of making database connections is done in a Java-written implementation inside SchemaSpy. Note that threads are not managed by the Effect system. diff --git a/docs/src/main/mdoc/en/07-Schema-Code-Generation.md b/docs/src/main/mdoc/en/07-Schema-Code-Generation.md new file mode 100644 index 000000000..3b31f3816 --- /dev/null +++ b/docs/src/main/mdoc/en/07-Schema-Code-Generation.md @@ -0,0 +1,205 @@ +{% +laika.title = Schema Code Generation +laika.metadata.language = en +%} + +# Schema Code Generation + +This chapter describes how to automatically generate LDBC table definitions from SQL files. + +The following dependencies must be set up for the project + +```scala 3 +addSbtPlugin("@ORGANIZATION@" % "ldbc-plugin" % "@VERSION@") +``` + +## Generation + +Enable the plugin for the project. + +```sbt +lazy val root = (project in file(".")) + .enablePlugins(Ldbc) +``` + +Specify the SQL file to be analyzed as an array. + +```sbt +Compile / parseFiles := List(baseDirectory.value / "test.sql") +``` + +**List of keys that can be set by enabling the plugin** + +| Key | Details | +|----------------------|------------------------------------------------------------------------| +| `parseFiles` | `List of SQL files to be analyzed` | +| `parseDirectories` | `Specify SQL files to be parsed by directory` | +| `excludeFiles` | `List of file names to exclude from analysis` | +| `customYamlFiles` | `List of yaml files for customizing Scala types and column data types` | +| `classNameFormat` | `Value specifying the format of the class name` | +| `propertyNameFormat` | `Value specifying the format of the property name in the Scala model` | +| `ldbcPackage` | `Value specifying the package name of the generated file` | + +The SQL file to be parsed must always begin with a database Create or Use statement, and LDBC parses the file one file at a time, generating table definitions and storing the list of tables in the database model. +This is because it is necessary to tell which database the table belongs to. + +```mysql +CREATE DATABASE `location`; + +USE `location`; + +DROP TABLE IF EXISTS `country`; +CREATE TABLE country ( + `id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `code` INT NOT NULL +); +``` + +The SQL file to be analyzed should contain only Create/Use statements for the database or Create/Drop statements for table definitions. + +## Generation Code + +When the sbt project is started and compiled, model classes generated based on the SQL file to be analyzed and table definitions are generated under the target of the sbt project. + +```shell +sbt compile +``` + +The code generated from the above SQL file will look like this. + +```scala 3 +package ldbc.generated.location + +import ldbc.core.* + +case class Country( + id: Long, + name: String, + code: Int +) + +object Country: + val table = Table[Country]("country")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("code", INT) + ) +``` + +If the SQL file has been modified or the cache has been removed by running the clean command, Compile will generate the code again. If the SQL file has been modified or the cache has been removed by executing the clean command, the code will be generated again by executing Compile. +If you want to generate code again without using the cache, execute the command `generateBySchema`. This command will always generate code without using the cache. + +```shell +sbt generateBySchema +``` + +## Customize + +There may be times when you want to convert the type of code generated from an SQL file to something else. This can be done by passing `customYamlFiles` with the yml files to be customized. + +```sbt +Compile / customYamlFiles := List( + baseDirectory.value / "custom.yml" +) +``` + +The format of the yml file should be as follows + +```yaml +database: + name: '{Database Name}' + tables: + - name: '{table name}' + columns: # Optional + - name: '{column name}' + type: '{Scala type you want to change}' + class: # Optional + extends: + - '{Package paths such as trait that you want model classes to inherit}' // package.trait.name + object: # Optional + extends: + - '{The package path, such as trait, that you want the object to inherit.}' + - name: '{table name}' + ... +``` + +The `database` must be the name of the database listed in the SQL file to be analyzed. The table name must be the name of a table belonging to the database listed in the SQL file to be analyzed. + +In the `columns` field, enter the name of the column to be retyped and the Scala type to be changed as a string. You can set multiple values for `columns`, but the column name listed in name must be in the target table. +Also, the Scala type to be converted must be one that is supported by the column's Data type. If you want to specify an unsupported type, you must pass a trait, abstract class, etc. that is configured to do implicit type conversion for `object`. + +See [here](/en/01-Table-Definitions.md) for types supported by the Data type and [here](/en/02-Custom-Data-Type.md). + +To convert an Int type to the user's own type, CountryCode, implement the following `CustomMapping`trait. + +```scala 3 +trait CountryCode: + val code: Int +object Japan extends CountryCode: + override val code: Int = 1 + +trait CustomMapping: // Any name + given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode] +``` + +Set the `CustomMapping`trait that you have implemented in the yml file for customization, and convert the target column type to CountryCode. + +```yaml +database: + name: 'location' + tables: + - name: 'country' + columns: + - name: 'code' + type: 'Country.CountryCode' // CustomMapping is mixed in with the Country object so that it can be retrieved from there. + object: + extends: + - '{package.name.}CustomMapping' +``` + +The code generated by the above configuration will be as follows, allowing users to generate model and table definitions with their own types. + +```scala 3 +case class Country( + id: Long, + name: String, + code: Country.CountryCode +) + +object Country extends /*{package.name.}*/CustomMapping: + val table = Table[Country]("country")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("code", INT) + ) +``` + +The database model is also automatically generated from SQL files. + +```scala 3 +package ldbc.generated.location + +import ldbc.core.* + +case class LocationDatabase( + schemaMeta: Option[String] = None, + catalog: Option[String] = Some("def"), + host: String = "127.0.0.1", + port: Int = 3306 +) extends Database: + + override val databaseType: Database.Type = Database.Type.MySQL + + override val name: String = "location" + + override val schema: String = "location" + + override val character: Option[Character] = None + + override val collate: Option[Collate] = None + + override val tables = Set( + Country.table + ) +``` diff --git a/docs/src/main/mdoc/en/08-Performance.md b/docs/src/main/mdoc/en/08-Performance.md new file mode 100644 index 000000000..00a4ab9eb --- /dev/null +++ b/docs/src/main/mdoc/en/08-Performance.md @@ -0,0 +1,38 @@ +{% +laika.title = Performance +laika.metadata.language = en +%} + +# Performance + +## Compile-time Overhead + +Compilation time for table definitions increases with the number of columns + +@:image(../img/compile_create.png) {} + +Compile time for query construction increases with the number of columns to select + +@:image(../img/compile_create_query.png) {} + +## Runtime Overhead + +Since ldbc uses Tuple internally, it is much slower than pure class definition. + +@:image(../img/runtime_create.png) {} + +ldbc is much slower than the others with table definitions. + +@:image(../img/runtime_create_query.png) {} + +## Query execution Overhead + +Throughput of select query execution decreases as the number of records to retrieve increases. + +@:image(../img/select_throughput.png) {} + +Throughput of insert query execution decreases as the number of records to insert increases. + +※ Not accurate because the query performed is not an exact match. + +@:image(../img/insert_throughput.png) {} diff --git a/docs/src/main/mdoc/en/directory.conf b/docs/src/main/mdoc/en/directory.conf index f0c596890..c54ff294b 100644 --- a/docs/src/main/mdoc/en/directory.conf +++ b/docs/src/main/mdoc/en/directory.conf @@ -1,7 +1,12 @@ -laika.title = "Documentation" laika.navigationOrder = [ index.md - tutorial - reference + 01-Table-Definitions.md + 02-Custom-Data-Type.md + 03-Type-safe-Query-Builder.md + 04-Database-Connection.md + 05-Plain-SQL-Queries.md + 06-Generating-SchemaSPY-Documentation.md + 07-Schema-Code-Generation.md + 08-Performance.md ] laika.versioned = true diff --git a/docs/src/main/mdoc/en/index.md b/docs/src/main/mdoc/en/index.md index 946ff7a97..99fbf56b3 100644 --- a/docs/src/main/mdoc/en/index.md +++ b/docs/src/main/mdoc/en/index.md @@ -3,76 +3,116 @@ laika.metadata.language = en %} -# ldbc (Lepus Database Connectivity) +# LDBC -Please note that **ldbc** is pre-1.0 software and is still under active development. Newer versions may no longer be binary compatible with earlier versions. +Note that **LDBC** is pre-1.0 software and is still under active development. Newer versions may no longer be binary compatible with earlier versions. -ldbc is a library for building pure functional JDBC layers by [Cats Effect 3](https://typelevel.org/cats-effect/) and [Scala 3](https://github.com/scala/scala3). +## Introduction -ldbc is a [Typelevel](http://typelevel.org/) project. It embraces pure, unconventional, functional programming as described in Scala's [Code of Conduct](http://scala-lang.org/conduct.html) and is meant to provide a safe and friendly environment for teaching, learning, and contributing. +Most of our application development involves the use of databases.
One way to access databases in Scala is to use JDBC, and there are several libraries in Scala that wrap this JDBC. -## Introduction +- Functional DSL (slick, quill, zio-sql) +- SQL string interpolator (Anorm, doobie) + +LDBC, also a JDBC-wrapped library, is a Scala 3 library that combines aspects of each, providing a type-safe, refactorable SQL interface that can express SQL expressions on a MySQL database. + +The concept of LDBC also allows development to centralize Scala models, sql schemas, and documents by using LDBC to manage a single resource. + +This concept was influenced by [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library.
By using tapir, you can build type-safe endpoints and even generate OpenAPI documents from the endpoints you build. -Most of our application development involves the use of databases. +LDBC uses Scala at the database layer to allow for the same type-safe construction and to allow documentation generation using what has been constructed. -One way to access databases in Scala is to use JDBC, and there are several libraries in Scala that wrap this JDBC. +## Why LDBC? -- Functional DSLs (Slick, quill, zio-sql) -- SQL string interpolators (Anorm, doobie) +Development of database-based applications requires a variety of ongoing changes. -ldbc is another library that also wraps JDBC, and ldbc combines aspects of each to provide a type-safe and refactorable SQL interface in the Scala 3 library, allowing SQL expressions to be expressed on MySQL databases. +For example, what information in the tables built in the database should be handled by the application, what queries are best suited for data retrieval, etc. -Unlike other libraries, ldbc also provides its own connector built in Scala. +Adding even a single column to a table definition requires modifying the SQL file, adding properties to the corresponding model, reflecting them in the database, updating documentation, etc. -Scala currently supports multiple platforms: JVM, JS, and Native. +There are many other things to consider and correct. -However, if the library uses JDBC, it will only work in a JVM environment. +It is very difficult to keep up with all the maintenance during daily development, and even maintenance omissions may occur. -Therefore, ldbc is being developed to provide a connector written in Scala that is compatible with the MySQL protocol so that it can work on different platforms. -With ldbc, database access can be done regardless of platform while taking advantage of Scala's type safety and functional programming. +I think the approach of using plain SQL to retrieve data without mapping table information to the application model and then retrieving the data with a specified type is a very good way to go. -Also, the use of ldbc allows development to centralize Scala models, sql schemas, and documentation by managing a single resource. +This way, there is no need to build database-specific models, because developers are free to work with data when they want to retrieve it, using the type of data they want to retrieve.
I also think it is very good at handling plain queries so that you can instantly see what queries are being executed. -This concept was inspired by [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. tapir can be used to build type-safe endpoints, which can then be used to generate OpenAPI documents, OpenAPI documents can also be generated from the constructed endpoints. +However, this method does not eliminate document updates, etc. just because table information is no longer managed in the application. -ldbc uses Scala at the database layer to allow for the same type-safe construction and document generation using the construction. +LDBC has been developed to solve some of these problems. -### Target Audience +- Type safety: compile-time guarantees, development-time complements, read-time information +- Declarative: Separates the form of the table definition ("What") from the database connection ("How"). +- SchemaSPY Integration: Generate documents from table descriptions +- Libraries, not frameworks: can be integrated into your stack -This document is intended for developers who use ldbc, a library for database access using the Scala programming language. +With LDBC, database information must be managed by the application, but type safety, query construction, and document management can be centralized. -ldbc is designed for those interested in typed, pure functional programming. If you are not a Cats user or are not familiar with functional I/O or the monadic Cats Effect, you may need to proceed slowly. +Mapping models in LDBC to table definitions is very easy. -Nevertheless, if you are confused or frustrated by this documentation or the ldbc API, please submit an issue and ask for help. Because both the library and the documentation are new and rapidly changing, it is inevitable that there will be some unclear points. Therefore, this document will be continually updated to address problems and omissions. +The mapping between the properties a model has and the data types defined for its columns is also very simple. The developer simply defines the corresponding columns in the same order as the properties the model has. + +```scala 3 +import ldbc.core.* + +case class User( + id: Long, + name: String, + age: Option[Int], +) + +val table = Table[User]("user")( + column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(255)), + column("age", INT.UNSIGNED.DEFAULT(None)), +) +``` + +Also, attempting to combine the wrong types will result in a compile error. + +For example, passing a column of type INT to a column related to the name property of type String held by User will result in an error. + +```shell +[error] -- [E007] Type Mismatch Error: +[error] 169 | column("name", INT), +[error] | ^^^ +[error] |Found: ldbc.core.DataType.Integer[T] +[error] |Required: ldbc.core.DataType[String] +[error] | +[error] |where: T is a type variable with constraint <: Int | Long | Option[Int | Long] +``` + +For more information on these add-ons, see [Table Definitions](/en/01-Table-Definitions.md). ## Quick Start -The current version is **@VERSION@** corresponding to **Scala @SCALA_VERSION@**. +The current version is **@VERSION@** for **Scala $scalaVersion$**. ```scala libraryDependencies ++= Seq( // Start with this one. - "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", - - // Select the connector to be used - "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@", // Java Connector (Supported platforms: JVM) - "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@", // Scala connector (supported platforms: JVM, JS, Native) + "@ORGANIZATION@" %% "ldbc-core" % "@VERSION@", // Then add these as needed + "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", // Plain Query Database Connection "@ORGANIZATION@" %% "ldbc-query-builder" % "@VERSION@", // Type-safe query construction - "@ORGANIZATION@" %% "ldbc-schema" % "@VERSION@", // Building a database schema + "@ORGANIZATION@" %% "ldbc-schemaspy" % "@VERSION@", // SchemaSPY document generation ) ``` +For more information on how to use the sbt plugin, please refer to this [documentation](/en/07-Schema-Code-Generation.md). + ## TODO - JSON data type support - SET data type support -- Support for Geometry data type +- Geometry data type support - Support for CHECK constraints - Non-MySQL database support - Streaming Support - ZIO module support +- Integration with other database libraries - Test Kit - etc... diff --git a/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md b/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md index f816b6f49..0579c187a 100644 --- a/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md +++ b/docs/src/main/mdoc/ja/07-Schema-Code-Generation.md @@ -30,15 +30,15 @@ Compile / parseFiles := List(baseDirectory.value / "test.sql") **プラグインを有効にすることで設定できるキーの一覧** -| キー | 詳細 | -|--------------------|------------------------------------------| -| parseFiles | 解析対象のSQLファイルのリスト | -| parseDirectories | 解析対象のSQLファイルをディレクトリ単位で指定する | -| excludeFiles | 解析から除外するファイル名のリスト | -| customYamlFiles | Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト。 | -| classNameFormat | クラス名の書式を指定する値。 | -| propertyNameFormat | Scalaモデルのプロパティ名の形式を指定する値。 | -| ldbcPackage | 生成されるファイルのパッケージ名を指定する値。 | +| キー | 詳細 | +|----------------------|-------------------------------------------| +| `parseFiles` | `解析対象のSQLファイルのリスト` | +| `parseDirectories` | `解析対象のSQLファイルをディレクトリ単位で指定する` | +| `excludeFiles` | `解析から除外するファイル名のリスト` | +| `customYamlFiles` | `Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト` | +| `classNameFormat` | `クラス名の書式を指定する値` | +| `propertyNameFormat` | `Scalaモデルのプロパティ名の形式を指定する値` | +| `ldbcPackage` | `生成されるファイルのパッケージ名を指定する値` | 解析対象のSQLファイルの先頭には必ずデータベースのCreate文もしくはUse文を定義する必要があります。LDBCはファイルの解析を1ファイルずつ行い、テーブル定義を生成しデータベースモデルにテーブルのリストを格納させます。 そのためテーブルがどのデータベースに所属しているかを教えてあげる必要があるからです。 From 9c1d06bf8420e11bdfa21034c05b46df7ea5c16b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:41:01 +0900 Subject: [PATCH 152/160] Replace Older Versions document --- docs/src/main/mdoc/directory.conf | 2 +- .../mdoc/{olderVersions/index.md => older-versions.md} | 10 +++++----- docs/src/main/mdoc/olderVersions/directory.conf | 4 ---- project/LaikaSettings.scala | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) rename docs/src/main/mdoc/{olderVersions/index.md => older-versions.md} (88%) delete mode 100644 docs/src/main/mdoc/olderVersions/directory.conf diff --git a/docs/src/main/mdoc/directory.conf b/docs/src/main/mdoc/directory.conf index 8b7b4d223..487d3c8ff 100644 --- a/docs/src/main/mdoc/directory.conf +++ b/docs/src/main/mdoc/directory.conf @@ -1,5 +1,5 @@ laika.navigationOrder = [ index.md - Migration-Notes.md + older-versions.md ] laika.versioned = true diff --git a/docs/src/main/mdoc/olderVersions/index.md b/docs/src/main/mdoc/older-versions.md similarity index 88% rename from docs/src/main/mdoc/olderVersions/index.md rename to docs/src/main/mdoc/older-versions.md index fb2582ee6..4a71cf760 100644 --- a/docs/src/main/mdoc/olderVersions/index.md +++ b/docs/src/main/mdoc/older-versions.md @@ -1,9 +1,9 @@ {% - helium.site.pageNavigation.enabled = false - laika.metadata { - language = en - isRootPath = true - } +helium.site.pageNavigation.enabled = false +laika.metadata { +language = en +isRootPath = true +} %} Older Versions diff --git a/docs/src/main/mdoc/olderVersions/directory.conf b/docs/src/main/mdoc/olderVersions/directory.conf deleted file mode 100644 index b3e21b82d..000000000 --- a/docs/src/main/mdoc/olderVersions/directory.conf +++ /dev/null @@ -1,4 +0,0 @@ -laika.navigationOrder = [ - index.md -] -laika.versioned = false diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index 4efee7abb..13b541990 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -54,7 +54,7 @@ object LaikaSettings { versionMenu = VersionMenu.create( "Version", "Choose Version", - additionalLinks = Seq(TextLink.internal(Root / "olderVersions" / "index.md", "Older Versions")) + additionalLinks = Seq(TextLink.internal(Root / "older-versions.md", "Older Versions")) ) ) .site From 7c34bbc449bfb37c37b0a7ac1091ea51a209a6db Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:45:01 +0900 Subject: [PATCH 153/160] Change MYSQL_VERSION variables --- docs/src/main/mdoc/en/04-Database-Connection.md | 2 +- docs/src/main/mdoc/ja/04-Database-Connection.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/mdoc/en/04-Database-Connection.md b/docs/src/main/mdoc/en/04-Database-Connection.md index d3f3e42e6..881612b10 100644 --- a/docs/src/main/mdoc/en/04-Database-Connection.md +++ b/docs/src/main/mdoc/en/04-Database-Connection.md @@ -12,7 +12,7 @@ The following dependencies must be set up for the project ```scala libraryDependencies ++= Seq( "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", - "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" + "com.mysql" % "mysql-connector-j" % "@MYSQL_VERSION@" ) ``` diff --git a/docs/src/main/mdoc/ja/04-Database-Connection.md b/docs/src/main/mdoc/ja/04-Database-Connection.md index f8516f835..e547e2276 100644 --- a/docs/src/main/mdoc/ja/04-Database-Connection.md +++ b/docs/src/main/mdoc/ja/04-Database-Connection.md @@ -12,7 +12,7 @@ laika.metadata.language = ja ```scala libraryDependencies ++= Seq( "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", - "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" + "com.mysql" % "mysql-connector-j" % "@MYSQL_VERSION@" ) ``` From 080c1ed63d9c6d64cec5955deafd4db80834be61 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:46:44 +0900 Subject: [PATCH 154/160] Create README document --- docs/src/main/mdoc/README.md | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/src/main/mdoc/README.md diff --git a/docs/src/main/mdoc/README.md b/docs/src/main/mdoc/README.md new file mode 100644 index 000000000..6f6928a0d --- /dev/null +++ b/docs/src/main/mdoc/README.md @@ -0,0 +1,47 @@ +{% +laika.title = ldbc +laika.versioned = false +laika.metadata { +language = en +isRootPath = true +} +%} + +# ldbc (Lepus Database Connectivity) + +@:image(img/lepus_logo.png) { +alt = "ldbc (Lepus Database Connectivity)" +style = "center-logo" +} + +[![Maven Central Version](https://img.shields.io/maven-central/v/io.github.takapi327/ldbc-core_3?color=blue)](https://search.maven.org/artifact/io.github.takapi327/ldbc-core_3/0.2.1/jar) +[![MIT License](https://img.shields.io/badge/license-MIT-green)](https://en.wikipedia.org/wiki/MIT_License) +[![Scala Version](https://img.shields.io/badge/scala-v3.3.x-red)](https://github.com/lampepfl/dotty) +[![Typelevel Affiliate Project](https://img.shields.io/badge/typelevel-affiliate%20project-FF6169.svg)](https://typelevel.org/projects/affiliate/) + +ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effect 3 and Scala 3. + +ldbc is Created under the influence of [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. Using tapir, you can build type-safe endpoints and also generate OpenAPI documentation from the endpoints you build. + +ldbc allows the same type-safe construction with Scala at the database layer and document generation using the constructed one. + +Note that **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. + +## Documentation + +Full documentation can be found at Currently available in English and Japanese. + +- [English](en/index.md) +- [Japanese](ja/index.md) + +## Contributing + +All suggestions welcome :)! + +If you’d like to contribute, see the list of [issues](https://github.com/takapi327/ldbc/issues) and pick one! Or report your own. If you have an idea you’d like to discuss, that’s always a good option. + +If you have any questions about why or how it works, feel free to ask on github. This probably means that the documentation, scaladocs, and code are unclear and can be improved for the benefit of all. + +### Testing locally + +If you want to build and run the tests for yourself, you'll need a local MySQL database. The easiest way to do this is to run `docker-compose up` from the project root. From 7ae262d6d0dac674c02724b7968ce76dadd243b4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:51:23 +0900 Subject: [PATCH 155/160] Setting tlSiteKeepFiles false --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index d00aad881..a92ddbc7f 100644 --- a/build.sbt +++ b/build.sbt @@ -25,6 +25,7 @@ ThisBuild / githubWorkflowBuildPostamble += dockerStop ThisBuild / githubWorkflowTargetBranches := Seq("**") ThisBuild / githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))) ThisBuild / tlSitePublishBranch := None +ThisBuild / tlSiteKeepFiles := false ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org" sonatypeRepository := "https://s01.oss.sonatype.org/service/local" From e4589c8a4f298f663db3b0de5685f43a07d48362 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:51:38 +0900 Subject: [PATCH 156/160] Action sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88826dd2a..50af60a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -379,7 +379,7 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/target/docs/site - keep_files: true + keep_files: false sbtScripted: name: sbt scripted From e951966fdc2720ecce6d9a01fa864b619255d8c9 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:53:31 +0900 Subject: [PATCH 157/160] Added comment --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index a92ddbc7f..1cfa4e901 100644 --- a/build.sbt +++ b/build.sbt @@ -25,7 +25,7 @@ ThisBuild / githubWorkflowBuildPostamble += dockerStop ThisBuild / githubWorkflowTargetBranches := Seq("**") ThisBuild / githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))) ThisBuild / tlSitePublishBranch := None -ThisBuild / tlSiteKeepFiles := false +ThisBuild / tlSiteKeepFiles := false // TODO: Deleted when publishing documentation for 0.3 ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org" sonatypeRepository := "https://s01.oss.sonatype.org/service/local" From 25f9ae447eba2abee5a98fd27ce95b9ced5e320c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 20 Oct 2024 00:55:40 +0900 Subject: [PATCH 158/160] FIxed link --- docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md b/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md index da33f7789..f488a0eb3 100644 --- a/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md +++ b/docs/src/main/mdoc/ja/05-Plain-SQL-Queries.md @@ -25,7 +25,7 @@ val delete = sql"DELETE FROM user WHERE id = $id" // DELETE FROM user WHERE id = Plain SQLクエリーは実行時にSQL文を構築するだけです。これは安全かつ簡単に複雑なステートメントを構築する方法を提供しますが、これは単なる埋め込み文字列にすぎません。ステートメントに構文エラーがあったり、データベースとScalaコードの型が一致しなかったりしてもコンパイル時に検出することはできません。 -クエリ実行結果の戻り値の型、接続方法の設定に関しては前章の「データベース接続」にある[Query](/ja/04-Database-Connection.md)項目以降を参照してください。 +クエリ実行結果の戻り値の型、接続方法の設定に関しては前章の「データベース接続」にある[Query](/ja/04-Database-Connection.md#query)項目以降を参照してください。 テーブル定義を使用して構築されたクエリと同じように構築および動作します。 プレーンなクエリと型安全なクエリは構築方法が違うだけで後続の接続方法などは同じ実装です。そのため2つを組み合わせてクエリを実行することも可能です。 From 34a29650a1631968c066df789e6282a78e38cc7b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 22 Oct 2024 19:43:36 +0900 Subject: [PATCH 159/160] Update README --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 94902468d..9baec942a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ Note that **ldbc** is pre-1.0 software and is still undergoing active developmen Please drop a :star: if this project interests you. I need encouragement. +> [!CAUTION] +> The current README contains the contents of the `0.3.x` version under development. Please refer to the [documentation](https://takapi327.github.io/ldbc/0.2) for the `0.2.x` version, which is currently released as a stable version. + ## Modules availability ldbc is available on the JVM, Scala.js, and ScalaNative @@ -118,7 +121,7 @@ val connection: Resource[IO, Connection[IO]] = The connection process to the database can be carried out using the connections established by each of these methods. -```scala +```scala 3 val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn => (for result1 <- sql"SELECT 1".query[Int].to[List] @@ -222,8 +225,8 @@ val result: IO[List[User]] = connection.use { conn => Full documentation can be found at Currently available in English and Japanese. -- [English](https://takapi327.github.io/ldbc/en/) -- [Japanese](https://takapi327.github.io/ldbc/ja/) +- [English](https://takapi327.github.io/ldbc/0.2/en/) +- [Japanese](https://takapi327.github.io/ldbc/0.2/ja/) ## Features/Roadmap From b94994bac0371f0cd8c8a0232a5f29d111025d3b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Tue, 22 Oct 2024 19:46:15 +0900 Subject: [PATCH 160/160] Update LaikaSettings --- project/LaikaSettings.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala index 13b541990..7aa542c4c 100644 --- a/project/LaikaSettings.scala +++ b/project/LaikaSettings.scala @@ -29,7 +29,7 @@ object LaikaSettings { if (canonical) v.setCanonical else v } - val v02: Version = version("0.2", "Stable") + val v02: Version = version(LdbcVersions.v02, "Stable") val current: Version = v02 val all: Seq[Version] = Seq(v02)

5tK3;K5bc4{my?EMIVJJj5k~7}qdv98M!mZFf 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 literal 0 HcmV?d00001 diff --git a/docs/src/main/mdoc/img/insert_throughput.png b/docs/src/main/mdoc/img/insert_throughput.png new file mode 100644 index 0000000000000000000000000000000000000000..97189de3f43284570eef5a853b059ab5c03d5864 GIT binary patch literal 53539 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 JEfd!B|36Z(3X1>$ literal 0 HcmV?d00001 diff --git a/docs/src/main/mdoc/img/lepus_logo.png b/docs/src/main/mdoc/img/lepus_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..076140c6b0e68b3e58e13e74cc25ab9f1bcf609a GIT binary patch literal 11636 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 literal 0 HcmV?d00001 diff --git a/docs/src/main/mdoc/img/runtime_create.png b/docs/src/main/mdoc/img/runtime_create.png new file mode 100644 index 0000000000000000000000000000000000000000..f6e9b1978b47a28cf24e6c95c84e0aeb5c8167e6 GIT binary patch literal 42249 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 O5O})!xvXc&di-v}VFDvs{6%Fl5CK?+0 zKK51M&Ox}K9qf-z`lWOfNvD#E}AHKtEx%@h^U7=deSv}@=$(5?Vi=)gZT^jl~sYQPoRGjxjc zYYX%T|2=~Ne8_d(cr>=ZJ|Fe=07-!V3WPZ4Ec8!jDF*N=*|7zeD zj-8CQBN`gPSJXFpBohGz8X6WkSpBKfQ$+$gRyK};E+ThNpAZDDQMcLd(wsixWGQm@sp1owN45^8G`y_rtn7D1acO91gdI%G z1XUkP{Wl!=OXTizCnq~WHZ~{}$_nLTwRJFO;}8%KU}NWG%W9gVpIQCdO>0R;Jdb zHcpOg9IPB{|I;`LCnpE+v-7bwj;uzfcOa-qSpf#I8JVC)v;U8y0bX4iddf@GAQvOM z|KlJhu-PA`pe8&Yh5F?8uv1?CennvuV?kpFQzIu^hYNRs&rVsbW@>-=?%4-cVA21R z90{|bm^iI2bUUZy9^_CHRJT9Omk$;$LE3MjG?R!*1i{#g)8CIqdFY|KUOy0Dm- zni)Z?obHM`Kx~}A)}{uh?`UdZ4>5IcrD37r_}^A}d6Lsr{=FXI-#aTPVd8jsJ^x>9 zJC!s$BL_!Qbyqu6k-MsE)I^k%os)}&gM)>O{e1k{NB_%cNf#$s6H#tXKJI5mTs$oNoLrnN+*|^@EJn|a zO<6d&%{X{XIGzcZnDL!c{XOJ=p(*tYAj!kY!^z7dz`@PIC&0u1XHBOc{yq4A+o`|h z>3`f6VL*=rPhZ#8LEYBYO7zqY3bXxv`_II}C{+x+#xIqP39N44-C;I)x|MqF#KO~zqfcu^swO0=B@GA;$egd z&h?_!jH6jE=bZ=v$5{_PvW@PN55;uvUZG)N)4cfeLtGGdTBgO_(Gv5zI2!teAGgt8 zs(D@i>n0rZVv-wCkHL6>0*aQnU0e4bnx|LdRe0AEG7hxFvHqfgt+Yped8+F&Q1q9* zPA9lof6+i|WDvV@X}M?^NLF#XxMc%$lD}xs@XwfEdV%ZWGzctgrQRc^50_sV4Ffat z@SlrJ0H#f=Mr#CDd1I;nMdQU}MDj(p059X?L`K7}xKr=~_b(dP6V|!@Voc~VFd6cg zV+Z@0zs z6+C?ONTblOQQh@BAYbWnvgC!=51v;$FTopquZfi6;o_>=*%dO=;auhb%^rrmy}h98 zdfJWG@AuYc)R6G>NTz6S>tt;+`PRW8RgJ^W@{p!*}IHCUzF7yT5)tr)Mu!?Bw;|xrR)zefhUA6$QN< zEw>$9n`%jn$@SP@TUrU^3KI zQj1gdGL>8T2KkE@*t^)*6ZT&e23%I$>n?ykH^e(iU$7@AvebasQ(t!`x~#ewY~pse zTpX9*UM4bmPuy<)%4gQg>=B;?boz>I7v*0>JTS49JjI73FRL$h4WQMNot5`46L}GW z)|gF*UH#YF_yBs1Un2m0e3{595e@$qx)IqWmWk7>0-Ei^Hy8XD5mDf_D9z&CzqUai zFr2HiV?>vUG)TXg?7Q{~beT^+4S?yqI-Vf(7m@$V?ElN`Gx_;nCHpNRXx}{5W@RnL zLi2g9TjVG*SeI(B8DBT-TkP)lZR=|8RUfkc5+yR8io9AIWS@bfmv$(>+Tr+D95u)k z4>a2=lA^EZN2lA5Y^c+(@9!PgS#=8aJgY={O?EYoREX{;c-+3M%bdmH z-?hUzH?Hgsh%hqBH8wWl5)$r+MCKj+>KyHF!wld!AY{~9)@yrzPljGOLAB?nkxt<= zf4L~eC=&s9=M^pqGw9M#nb6uE4RXVFLZgLs&?T6VLQw8el2>=}%J}oXiZZjVbPT*sk{C4^7y4HE$)sG?KlxlTbSBQ|TErkTZ-4c?lfZ08+TM}9w$YPJ;4~7X z4cT?d8j!YD{q$3^%RO_YK6WO-eR)Q@zw}_M%eiwzL!)%Ff8=n5m&Kq=3jbC=yaQQ3 z|8*@~DOBJvbxyI}ih##@Tzug!AM#6i3-X{lOsnoBeJtR z?D`@@aZl3TSlM6N>xGYDma1XwkN`M!F%(E z(3D1om27{O2Can6%50*;vEv^E1Sq(i=Iid zrQyGnDuM5JYm;$XYEf{)~u=1{RUA26-`Ho|o*`J)kRYPo|qv=lrs zeq}8g#_8FPhtHBn!$q)dHKt9^Hl;+#I(~6Nns7Xq_+`#L-!!Q$-Al5T%HNg0k3Jv!v}5>zBly+7&9td6DID2hSw2mVqXEPEssC%01QHEzf7Rx^eFWem%cr~N<{NOE4A>r4GdB=|)36CzE zT0Vhn*Ka52Hp$=Y+$B*o!^cq~7AIlVd(tFN3aQ$7eIHx>i+@Gc z?%eTH&(M>#o~UY9cvdn(+(#wqq5j^IPY_xdes5U4*qLOmS7i8ovGI5mi~fvNRrlcA zZred6M5$weISvK14g+f=`6h+bEQ)`(KuQogxv>hWA474H^F27Sv?l*@U!Fi8v>L72)^3BSg;ot{7aP5Q5^7=Xz#D0G%q+N;Y zoho0`qEPwh;AIJ22PY{J5aVG9!N*@Ur6UY6!FwQVIIMjAGM z^-L+@&9)@h&*CTyN**s@W;mzZr-&I*hj}n`#);`CR)H3r@Ck5O(xepUpLb||xq+3z zyMk9h(ARE@ui7c7p^;^Wo+)oP;f3J#?0TwfXt4pB_`&EYvh;|@a`<~JR>ku*cF^F* zNVCIza-VFhOKzrk6w!y7&ASHal;-n7= zH-5eOL_^=W#g7&5Y?)6_AWJ>0_$vxMe?3X`LDv?yZXwg))(0%6j0eg|lQ;D`X)is5 zhEa#XOgqIC7I?&IDo=NhO#7ln148 zdw)@ZD*zr`=W~XCFKvrBO(j63HS#X?We-z44hM*S$yu=OUG@X-YXMX$Ri`g1D?m_& z3vigZQ_~Iqas;Pwr{!#<)-U_?;5(~3`p97{_-9IZD?hRgZdU)UFHK!%EQpgU2k>{L zj?A*iE-YvG@nl9eyxDjB&5?ch=AaGuKxEw5%G49RYRl|sC5HZjyG!#0_pp@S-f$lF zD>xONkiRShI$ruiBfq@e%N%Ycx%q&emCS5^vbIgZ6)+^!ckYGEkG>{@6 z=mrMa4I2L1P=dLOA~acp!L%cnswlp{Ck7v%8n-G9kb^cuM_Hn}-4j?aS?`P6x!s}c zy13QicYUtV>osO2+Cz~xE7{+LRlD02p6JL|rKIif>tg>8vP46H*TGr5IPzkXiK|lO zb6;7FQ#6i@d)!*ueau5t7$Y5Gd0~sbea46= zQa=J`$%u@2CH?^wGqQ9YUDywqYd%M2l<;naXpm4;pYsn$W)YU&l*|f`1VcDQrN*uT zA)t|)Pv(*?yb7j_VV5TI9#>Q!6ep&O2&M@Sj>J$Wvhi(@L`9!3j)hivte={b+RNQV zxY|NXSNa6salsyqmM_5z@1&L(9&;x0+p7X6-S;EH9nFQZ&aO(cpRcmx*q!~$tj0e+ z^ux=tt|oO`Y;{-93f=3~cv%9RS@_;lYWct!ROv9Ip4trA{5pAXQziZ3OFgR#(cvUX zVSZYwR<(|7DuGLg7S8)+UY0ayEUls3^bVCdo=+f-(MnXaXMo!M^PJSFP`WCxNfsph>UuSGMVBloHQ{fxkt@E?jleA}NO?Ul2rS{nJ3 zhNBje18WT$iQ!1UkI4xbE2L{(>>b-f>Cy!ad8aaq(v2Ngm$@wtB}1JQb(LDtLk1Ey zmB+UK9LYF(>3_(iatBcgma9B@61u%Syix)id1+gHK(|#{oZHK|KOt?<50}j4Ms9%T zJ*CO!>U^oGXE8IhxN|ejUkm5mdlS+&I|}&9wfmp3gASdZr|f_7JwFjbk1)sb)2QP= zI3+4GAw@DG`;_d|zjoBgKcq|&MuCRHx$l(!Rae{Am?O|4xbN%=* zOuITKC#r4)hc22;H}z_e1$cJT0P=HJRwGYc^NX09zJSB@lipeU5>|(BQ4jZoy5z%R z;sl%VlFr_j2WzjiG#EY~n8T71uRR}$Fu2J#OFOskX!(Wc+&KE=vD(#X!k1C2vgwOb zG1NYirfC;32xi`SoV_XTef&vpsi5wWh8!q83g33%*L&h_-F3hdPyCe1`v~QT?_F@j z^*ot0o4k2>^FQ*kLo#RcZ#B&YC`8n&$NqeW(aSWoK`H3+xF^26nr)8w{G_9yz#h_s z4@S#Gd(we6^2-lw9vdMeXW>ci>9R{W3dM~f52HLD>nL?^l-6;(R5(m`d`y@vH6Iie z*=~>*^!W9b%JnDA5jIrL*X+p5-Er6+N<+e6A(a?E-8FUm556NM)V$Ouk108q?KPjl z&-I6o@R%M*hjuk5@|aQ?j%8DXDxdL!1`ltD94Z}eQ=MEr!Xd2ema84Isl-*;10fN4 zGbB#a3s{Dvxm-&-R~FBmjh3#(N;a__A1<%Wd@a$QM^_Hwv>m&<%~~Sq6qb*Q&AaJ@ zt(ueRMj*|pBlqTJH(H;s4{>(Koy2y$!>jz3#4=ib7@+qVd^S0d`Mtq!_wZIRolJ5b zfofVg%i~CeQOSB8^F*yr9?Fmu+Y$lUB<%4eIjmCGa*hivmdy;N*wyXddE4GyNz@JNmtd+M=?jAl*pHjmz6Jh z{^W!8qDE`^`8e&B-?u**(>s07tihX^yM1n+f#ewJa%Vmp?`;2IJaP9kC*w-uc@1O* zj1>aUR!`XDaV%fZ>TJ*o6Eed_uWpkDqHppP)@&6VAT8IF<$c_bRLktJs+JTn?;0{> zuY1q=ppjn%dX?g96oZ!PX!$Z{_`ms@k4*=sCEs)|R-vMU;l*hg-u^sL)7~ZsW;Xn+ zSNUG!-mSI0rIGAv=Vgsn)ux1vWe+UeS+%BB&aO|>bu-<`THBHj&kxaWPcc5xjo-E$ zpgnq?UcxKE1jg#sd}cmYN_;!=TXKu~NSzF}i6Z(+ps@DF9_wEAYkcx-U7N)M3Bvlk zsTO5^gy9=}@|g(Z6GkE%v*A+k{}?`)4$>c)cV5N+wUfuiWs5AL?OVm&VDpz9EVWJ7 zn`OJLLigOWXBtFZDy!DnQGM@?vRxPStpQ>G)e0Uf8q1eI|Az=7eyva-(fUI0$eG#T z!Bs!ARGjvm2;N{&>jR#{23ydBH$d=SGxT>i_PFLYR4g%BC276|O4OA21VpR42~G$3 z`=iq1pTB%jbJ{VOV^*EBJP-hxZ%pGmV>F~n2Md_uyL%TdduC-8Q>vt-Z!f*;vGw*$ zt=h`9T!>1v==l);Y&&tZ{MoemqyA!Bb7jvY^39CbgcJ+IP+Rp}@{b15UimeyAaA00 z0na42vE9(j{((d`Zy%4iYL}Fpe)bcK_kRvW&d3I-jfI~ky>)BboZuD}+x~#~wNz(^ z)N2c}B%svx8v5afG~w8&R7%%BFO_74qc&W_Ia?j+a^nuRmmD$LzCRgUEN`uPAVF7s z{HuSEyFb{vn#8y{hDyXufyY9l2NiE5?l?moU;&3(D|Mn$(R}&263P>Ex0!CMK3Nhf zb*`FR=9^NmK3N&r$^rk1SI&}N3C3Bb$vh1G{&3`zXL+xnZk`Jf5G*KB2e~(Kb@l5% znJKd4;bej7mM_|w;j%t?P_I*Flcz#wuvBGc!TTp0FP=fPJ07_0=9IKq=z4UUg;I1T z$4!^(ZU^g?4I~6!N}|BTC<5i`gvv>Fle`TIg19AT+^0&N+0^R}pd#mUD?%0>B;edI z7|1V=Jtn<>ZWDm`;`$I+M@o!$N=A&gH^53@vGYmU3ovc|%q@AHBu!Pee{^H@8_Jv6)B2xvV?s+Qwo;%MZ2gl zuw!Vs$Q|8~VppB7f~u3AFcgpdbFrYpWcR(J;sALIoQ5Mu+*P65z))C29FW^nKc!cdE%buYWb#Vn$vVic@$5_diYMfCS0iFzO+5h;{LGT|AYx$V2DLdMIwmV~Xv$=X z*5I4cy$NeF*1fytOC4pt28-VB^%A7cRbQND3VR4Ac=0Jb1enWtG_j2&Vwt0TWI|V( zu_-RihE$ZO;=a#KtW3brSa?on$XC6$ofR~*e)4Bi(J*2q43C|)n2=wyzLwUGlmu_{ zErdDehVAmZW7F6nF&J=_*p7JhE@a%qXK(O(NjX3frBiZmX0!^LLc?cKqlHd zFU04ucS#aNO{-?c?Yldp)x)J%yd}iE#OA_aceu{&EE09-;Bsjn_^K0;@hsEp@%f3n1{GZcqDBFVd<>aqKh@I8)=#dhHK}nNApo9j;Ypg zgH~=LUVoJwf70WG;Aw9tAyoEA5CLLBkk8G0$Ex)e8A>r{J(ADu`VMwg_MRo%OF=3< zKP7LfRC>&d1~IUlr&*P^iWsh+Q>R(L3Djo6=aWEAxYu@*K8y0I>@5i;U}s4u%4muJ zvNNs3*t>$6koj1RSHx+R#bk`oWj%}Wd57+vJKZ&zY@!F_nEsXhCu^hqD?Yl3jzk)a z-vC?urwW)B{UQ;(<4GJE%vrp=EV7(03E_I}1PFd@Z=PaYcY)~ZaZZ%*Oyi`roy@!z zJ*Xo+`ZR{~@J-#?E4skM6r)E{t`@7izK_)T(?Z=g=O+c4uU_OZX`y8p!HN7n+$!w*q+ z`4ePgmo$*Qhrz17<0ktOac84!_==ZB2SQ=hHpHStGRo-nzP*V;+rK)%T-{}|TNM3$ z!^P&MR-a-=I?@K;h^4pZXu9JAsyESO%SbhV~0RVUI7tp*V-ih~Hj=rW9F zI_mE^iNq%{FubG#B>i9K=yYOB!iKZrM-aJO=06ig`)$?b0R+Kjpmgb!G8;s(53es? z?awl?`7sP)TZ6`R=vcPP-eo=4N7f0#VxjUJIf2PHF**HSOf+VDwn=o46;!(uz_Y{0 zy@<6NpAgJRy%T5(8H28dR8_>kw1 z&Em{?0ZQ&G3me&zwSFr<@cKLT@oC>=)lE0^X>gT zYg-e!knR6t)g!8iAXm4Lb#g|V2=W@)8y;~ch`*K_`NJh@BHBSbMnLGC);JTb`X&V zH9Upkw};aWmwAd;fKo8c0`4wl0+o=n-jtN`Ha1U1P1HPJPPVY?hD-3pI!1#ZrYHy3 zN}ZXG++d5FBM*bArXM>9YMb*@q#3n(5K6^?PvS&fe z5zbXS69e6(+vgUy0eex@f{uG%BZptdtv`@S5!Y$PEeaqsadW-GJSQ)qT$dO6S48N^8gU%*T5KtpIdqMHC8$*^la@OpE zpGzXBc&6m784dTB5auscg)&Ogv~Rrz?76l)ji8_o0IFc{Z#i5ZurwZJiHDl7s3cuI zUm;Bs2JeGXu9cDHY8+qHYEdMy**4UYK5@-4Z*KN3(UJxG( z6+6@w4EvD2DM1`DLTiLA*Ly(m^BCL23cbF1%dDhDzSE|bm!{@N&Zr0?GS#^Q6=8&V zn2f$0+uC>MnVZ9!OYpWcG^kx4pt+E}Ku`qT8XV}@BaRR%U$DIj)EU*7-rWz3~D#O8{q`%(&)+41T>2?UgaDekrh1z|$>p+I_H$uRDoIYF%zMo;WU z%fiQ&eL-dZpNO@Q3ZsDHTA-AeN!zzP=Dee*R97x7nk($;Tk6!qsJQPD7v5c0yRrW- zJv)m^PL^omW=2LgW$wUW9H|;t$xe2QzOpky7^~>qx7oc@XqUMO+as}=mIL>TYDqW? z#Jv3k5Ac@bW$(0o2|wYE;6<3~sOWyznZL#teK&LoQ?)OD2T4EvegB)H+su-L5?`;p z%E7NAw#}ul$$cB!T^3_`$LV^vzSk)K zp)?!3aWzg1`srA|OtZH>J|8_~;$v*DcmKO7dW}!2+W05GTmgIfrM0}v#cOw)-&1VQ ze5)=s!0~8jqQgNn*?mt1vgB1;3@Uck&|vW_&=Y@s%n~n5sG75-`Je%h#Kx z>OX~EO-DVO(v@3^Mb9{ZfifeOtKTIKJv#L@9`HlRtj&Lc%1k2N`LvU-JtLrFGH^31 zLjM_UCVQJgyl!>GxD{7PtKh9u&RCw`o|@6izC6==^!%XG&sznR2gUZqZc2vpT_ZIb zz)eWB^?pmuft>n#r6UotcJrjuXBiT>++=c$;}tb`HmKo9W)=6Q zT=v6{B{}FbfL1wQUuEJyn#{K^D#(j)ULH!7UUt{#oDvlKcOwkNWna@^KOJMnM`>@iq3Z@v-0wOrKdEyp$%)AKK?r2e4 z`mG%2y5ifY%ybyrq?TRW;iySc0^#^x$+PXI`8xF4zV-R$Xg&K0ZC8=-&3$J21pzAW zYda9xAbUu%qgHcmvbsJrDNS_A?GaEHlbyfWa&|KK+{fCEA$MuXFRkaK(|2u1td#l& zP85XvC|yRl?rq{?Cbsm(`vTc(R>P^636&1|!p3y1?*ZRmW1-nUOSQ2;I!{+@>gb(y z1UaFQ`|JwiX_!~u^V83aY7-M_%hg=0iD*|Z)62$`2E_yJ$MbmP>}_`%`|_EXBiK>l zE~z>c59xE|&5cN+VzGf?Xfo7$#Fy9hv*$$3JKU;F+P3{FA^S*@zsy zm(w~Y-v#C@V&3^{vWE1nr(5>zEZZb) zw3Ego7Zr+#$ty*3`IC7KK9^X1vRt(39D!|IsHjF%>JUY?3k?Gpv7B$E6p7Z*SX2(j z`S@_p#Na%XoP3C3P{6ei*2TxQ+I%C4XP zPgKq))XGP%wJR2%bHG+zt zcoh?Pr0Er7GH{GDzbU=!Ylz^1v{|tvoJ4PEH4`N23Jrn=>mnUu>+K%LqHQ!auH;0= zmbxs>Z{7M`Q+-(~FXRkP4H-XCWsCKol_S*vU3RrXz@S2W#pBpT_@dU>E)Y8HQV_G@DFH0Z(&H7jXjIH zb^kgEN&4iZq2HS~nXf~5VIi8nIZKH}DZfp9!craL+Pr(QM<7UPu2K4+EqJb`%$1r& zyW(fU!FFYCuIRr2q#$y3%0 zI<(4i+g@69kMTT<xXJRAsHC0Uqg#*Wl0f!q6gY5d-;9%!x zseoTslAw>a{V)?1%dgM28xL46{WK|Fh*V1AP(>vi%?{aH0&Dp{z)j_Iq*n-T?Rp>G zAldqydqpG1$v!g@(muPys9y15q|EZ8`5Vzi9EByUnYL(n-rm#}ymtlMcxVc^wrM%) zc+dfrCFgehcWox;aih$;1}H_E^sH>pTCdToc)x2><&h&5Qa97nWMHWCRP{a(>f;Xa z+6<*!)vyV*4S0_0|MSM#JJNtK%VG>Sse1jOvu1-!8*w;dC+>cmdf`sl1bmWcKFoJd z69wPIybWf@K27+RU)}BclP*{_F#L75wJtbzxrVotL(L+vPO~UK10}&UwU|JT0AtUY zvS45xpG%$L<~q%{|8(`+yW1F6nrgF+8hV5;)) zCvGFGf+&vixrnW2a!5Mf^--1m!|MR z#jCT*{S#MfXNvs*a(X%9IfsrW^oO-8+nL<XeD?DGsAWL zDwFx4&PM>k34h_9)O%ceErCZQT#eNA%ygn-9+JHkxPuUUG{5U>az#z(M^C$!9Mk^w znEe8I96;hpQ66bo|K=BHvk=6xKjYJ)&Iek#II-5)o3|52wYR_8bRU`eD8-~dxe#@O znbnv)DijiQ2Oj`Y5mz9%SX>t!yg7S{}rV_Uj0K@XGLFW&{F?;l~Xtlbhcg zzfk1>GVD@@peVB}5SnQvUK~F^^?*$$!OF0S*zXD>h(QSiZ~K?PYW=Rj8rZ!WRB4V} z7qzDJ_Na)LdW3uJ${XFTBkzba1X|X~zGeQT^-pk2x&X*!Amg_hL0?>FPwc3P;AO`L zw2Il9>8OCh2i&Gn3B-r+M-3}cYSoWBG_2#Ek1vYxe3&Rk)vdV7xVIbbm~-CW`KOMu zT0ix+z+tdo@0Yjk=LIryRl_yb>;ot8#vQFK7>9hJGCS8=RGT2vuBi-^5s(+~z3V1) z+=2_XJ@RA)a7gFDug&aUSyEG7MQ;O9Z6Nh=-&K@{P)brV_Fc?N5-O^sxF%JU**Y!m zhknFN0@2i#eAA$<){sT}~Shn&we`oBpCzGj>aN~M8>7PQmB7=|J>cF^o zQ=BMW^~Fikv9m~KV$LgEUYKC=6}j7g?um1jH`Gn3CMTyQ^HwLNT4ZT|DXBHlRg?u@ zR$P-vrgU->fMM;sHhbhhJq}r1=aj`arQB~O)@}-#JBI>d++}wU=(BuXauBoNOgIoZ zZbduONgg`tNQ|1@5F`6Wf(@cbjjyy?>%C5W+qv~kDF&4oxEpQy;^q^yzo3OF{NlUUaD2U>S)5~Z4f4a{#kmxxJh7NCo@UCTm6Feh(<(R zjRY=_Wef_^?32d`gpaq1#WYV2_6S?(0Ssc*R8qV~fe}6+)d?D%F0AJ!vJ+FPAPO)o zlsw^7Gh86(1UtyZT1y{gD?#&;l@oiOIF{Rrf~}a~``kVjmFjtBWB~vm)jG$=3jz{unr0TcdZCD&GYgz`Mf4w6Y#82tf{qa5M@uwDWcN zG=GFXoQqiN2z}En5H)fwB_-wN?b~^)tE+viaqH*SLY?@n2*Vby{8xpEX~rTI9I!vK zS@(F%;G^nN(oGcFE3H)A$*H8*FgDDk1JL{JA8i^B@{4BV!H@dPj*8uUH{|9&ULi`L zgBS##yY%F;hDe+2Tx4_gOku9(36Ki4*a%dAH2)Hyhp7+jb1#tY7{^h#V&M4)A8x|x zVpdxMrpcJy5HEiZ#PEMXqvZr*Qg7#-G{ zNV=7-aK6iGxL*@OZH`ppNS1QE0yyD(bnGm#VrrrUk*E~4DhMFtG#D%*NHKS-Y=6~z z`TIt}KMGrNsx;>ukHxa$8>gs`cD@Y}Y2pe>CyO_orB7K#?ejbykT!{82I=8)l5nko zBilc#)xVB7N4Nu^2%wsJvQxc)?mrh^$D5z771{Zf@Nqwot@7c%vFgqcTUhwHstkme z={DiXC7;c{XM0P8vFTj9eTWMYE(OUy@fEDat5OJkz>{_8%fKeYDgrjLm&?2>OLpVx zzUl1B{$9&OUyT=EsYUPOJ9n_UKe?>$z3CEYJ`-1Qnp_;_5e5Xe;)wItEfeKX%LC-dh-zW!_GX^pmSxJn0^1Ezyw*vA_5X@gs zTPJQzg|W%126@~on4-ZAVf zj${Jf_29=x(oWsJBH_$mf=g>l)x10RTz%$hSd1HO+ffC6eOJrSx!uB#soc}Z9pM9K z<0gX>;I#6K6YzeSm#xUlzuZ@r+?60YwK!V-jfX;URY=+~T0zx?mQ-@1q40Mw4>V=v zjWI!z7?0e)YrJcrkh3jOXu-8d^=Q!$k)3!wI_a8@m;jj+inhqIErah z;-+t*U=T6y!`1hS{B=kLL|Ubf0YnGF!PtA|TkNApua8>5KMsG4CT}(BPTh>SC)1wd zrTz-+gQzyIM`6W@Ta@PkP_t^XP_Q17SpaG{!CepHO2}eRC3OK%AqW`; z02N!@3$4Kbpdt+N<4iX&l_mpZvUikW2Y#6H2IDk{>r@p7j`XPY-^1DM&F7OW-Z~aE zf3L}WLVu4uO$RhTP_SHr=`H+&M+2s#;<}Ex&>TM^I77|Lk~t?o;ki-l*ycRt`P|Te zhbR1T?cpp)@)BKucElA!Adhyfwus3E^=Z-u_PZMX&; zPaHGiORempp%p8eF#@&7*P7TX2)9LnEy&2`tTxV)kjQ&>(RpAiL zx`X|H%};6Yl^*w0ILREmMS51;xZnvU6nVnzdGL@5+{v;=8$T5@;-^;HQbRehmsN6IS%7xMy@0ddlT{v0k z@Gx*N!}clJ8o|6oGEpy%1aJwu67y1_x>86+w}Nc|H-m>iQ7iElHszh!uymvBW0$U&pNH@anw4z2ECqo;N?{N zZ8rEk!YO7j^UfQ^*6`@7i2_blFV&uN(XTz#7})m*Hugp>(=ph@C|&V;rr5 zRP9dXy@CA^CXGz}(rKjWOH@)mDiMFDYMZ5hy6vh`kZtXf6g@uzaEhH{H2WN8HAeG6&+#bpUZ6Lm#BxqQ zl3Al;7&^!Fggtq0N}PP|sl&wjOgq!T8SGbl9n0%olnn>MWhdDV#C6CB7Wd?0Dfkcd z4Zr}$$z54Lel>0hUWlb1chV0aWv+k4#N{<0*6Egkf^&5r?O0Qa%-^!z2f~6x|4~{a zToTqvtAaxzRbqODe&JrkU`8+qW-m$~nE2DlT_1pE*w*vv;LV-PC{Xo(bhMnSSt3nr z&ojPN>YS>7`w(VTmu)tSc>jf-e@^IYv~hG|w$T;sUiWhEdh^a#a_@@s7T)GM2W-5Rafz}1&xMhp^%0p}=|yzHXE(|!ackNFP4>%D z)n5_FWZzfd0DM&qER4D?u%=&0w`ui*?gX*%*hOStcCXG(cRizC#fRmk3AGh^Y<9BW zNsQE_a;QE3Vi9||d0?L>08qs0MU)wSJj~Il4ZB+3XRs3J!AdlI0WzK#HN-h1mt$LR z6DMk(2lQEz>HVJ}_01ZOS|iW41g+xggxeIe#{) zk~(k%6=BCd0j4iGF)laH8g}FQ0Tqw@uiQz~fYG8Pf8_PP?yVD|LMmp{lRn$x`3t7c zOQ)M4jLJPEg$9neW_=6szo1AuU%BZ0R`{9xGWz^PZf}0+kSJ-6m z(E)Q+4u%`J>!m~dL+SL`NH`Zx!#i|L&R}u>m%V{%o))L7i(-C)K?6*8WR4#Bok5EV za82Lu&vkc`qqN6olNbQ`i@7fcqV@=Q7W`m!RS#KP58}&uBC}sr+gG7}YdkdN_a*^o z?yZ23fHT&n;yk7ft5Vo)mQlA6Bh@}=YOaUUhX3ZN#LO_k8x)C6wXM)(H^<*t*+nOi=j(meaW0H|e? zZvMwIxCV_8-}pLKI)fSSxD+kCRBr5)Et5Ae9rPZK04xuc3uAd z*ydaMlQ6NVIqRLB-jYnI zIw6T`#)R}aM*+SQJacZ~`3Qp>l;-C-$W^V$YJ9VNI1YTm9YkA;kc%6LT}wZ%It@od zsZara-EN}?lhss>E8N*yI;1r%HUduoh>r%_Z|IRH&SmgNhHfLy65 zodyNE)k<|L7KXIJ6Z#tl#Ahl<6Z#O)!SvS#GPyhQu8wl|Mq=KD2+CnOW)_OGo*p9K z*fG4i2Y^-_NTyz8hc~SUJb1}nSo+~Z#bkyRoiU!BZ2>DI zQ0$A{{N^+Txt0O#a|&XkWv_ieNsbb31=>kydp4!kNczFnzQq`!+m;`ZnH5q8fc8Fh zX12(cLTq(ga0MmFmX8lGs5L&(K08pneWH&el0d(5-YiuKRps|h6>S!f@thh78BZUe zy?;-45tiuQ55$u|6CPV@3eeS4!@qv+iL*73lBJNz8%8BrGh<6OhjQA6@JZ z4ZOvipb+YGG3N@Kk*rsnLC9u+VW){pY!kgdNwN(f!)3~#ofG4wy;>Pp#t%K-&V_+f zfV%0m`|O<+21Gn(=k_$N0#9ylKR3b!;e-4j1rJNY%xK-ZhibzK)y4wq-wR%=t8z2k z8|7;{#TJPhMJ)?`xiFx!YD|0g9Cp#gWVcz;=N-)L9KBA6dP>u0C&}Kbd1p&^k_VwHFZTrA{bzQDr6+^Z;lvxDZrz4Mv-K_w)%ua) zxjk3aQ_e-dm>|AOc^Z7jHqx{z(8`hYYl(S$kkU%)^oGP-XNWbx96r^M>oVp(AnEiv zu5|4lm_PS}<+#lUckd@MH7}hSEmh{wW#YRaxlZWHSf)Pc>aSI!4TJkqX7B*wkIko5j?Sw;EEZP(Lw9RPFzoOh_? zIN%5e{{(^(BPro3W%a~}5r>b7LDWF0-z{iT=^%a)|au3Yf0*6_^DZOhdD z46t%PLA@rW&P29t-TqpN##o(o^i|Q-l(+$8%GK!7L6*dQ%Puy&uF9_9UQf?&GNm>i zx$_z}na=t979Cl#g6`Xk0@>0I3nBBvkr4fEdA4>TLC5XzBF<5NnaT`i~X4zM1U?<%v{pBYmVhl3l`O zP%wyiIuR5MqI)=KA!Vz#q;mgo6x#o(ou|uQ_c)*T`Oz>%iOoob2A-F02IPJGW}B|B z@L>0!zH8x@yMtDJ03zi1a+qw9WjiXI-te3L?iM}f0 zS^rF_;Cu8$?Ob*|rMyKrei2ZFD|so3{DADTsy-g+%aeT6J$zU4kw&eVsk!%-f7Y(@ z;O^L~y~THCEFjA!*!r~NOgR@7{XYr?Rx@qTUz0?Vw&TkYtrWk#HgXBWCjIPIE%=K{ zSVIZmCQREUOodLQ-U>v?LZe>WXiGJXEb%{Ak$D{S>YkoGSW9idY{cJb@RU0e?C7x z^&2nZ6M&aM0R|n$-FF<)b~zbe4ezz)bL(|BkJ2B#^=}l-(_+vQ_wA*G=`HfDN!szo3QLT5$l3zL14MDZp3~Xv3Dnz4`^kU?zgDZ~@25Zz4M4vqQ+I;RH4;)>?$~4&% zYemTz%8Lc!1ijBA>6NLx16tV!Gf}0Y2LLbxmi`RZ#@;=rB`NA*o}lltPMMj${^ipn z^-7DT`|ZUDC;|y&L*y!`0i=e&jHsEeYPt0=kTz{vURb;@&Fj|`J<~;P*(M)jMfS6T z%f9Olbo|vETZ!jL$G-bwOzf9rR1pC^htvJ@=@ z{91^Vj4adqYAwTP;$ScrF&-gY(iS4yIUHhyIXS8NOuU* z-QC^N4QHY6`_=FKajxUFMA(~a`>c89nYm}?o>>-+$d>#k6&Yz|)z!)uvzu;}dMiP` zYAFZ&j`kDJvvf`fYfLjQYL7_`i~@W|L{3SEiM-kjVJ$ml8ib(I?qArKET_FBO@)!qbNVb{}DL0L{jEhhw{P zktl{T&`{$nLzVAZdNIh%k`8v}fS}LKSQjdx2pkn!NeUGQ-%TOQGFqwwUk`j@X?3J4 zHp5k+7E=Ya##~i0gRCZs61vU!K#RUPKM(cz`fD=#rS>M}A9j6J#TCkhs-w&CH+!j( zLo;+~c5eQKXXlk@aww)EhTi#^9qo3hYK#t@rRf+6E-`=Nc1c>VzgXmcthGqTcDlfi z-R?;$)Lv>(8H6gRd$Za%-=gFk%46^egZZqZZ>v0)$u>OjB!ocQw9>m4GN+4YGI z#UfL8GB&!y-L|>!qPfm5X@Z`PZUidaLuJYh-{zU@U4Pj{9j$W7GH zt7)H{jnFiAW6w0An}Eb>E>3z&k(u8gfrKFY#FX~fk}14wuux+yJsZav)qMr8f@tyE z21N`!Re9T2yNqVzMs}74CaVtA^W>X$gET7?o@PO;ykgmNjbqTgLD2V#!BO|9Xq{>$ z`wio=y<4NSMzai_EiO6MzUYgGxMX{sWh2X?oN`Pnq30q!;d-z27gv0wQB4Y^b38nk zJm|LX9Zr@S4v#Xv5efzF4nLO7sojuABJuoLYIb^SO0i$aD^0g|00mb&s%HOUk2KkDGGztim6Il%P zG-S=o_bfO244>s^>w-AtB)(Yuo^cXEitsQ?hSN{4{OVlA@XQ3ulOYzQ+t^jF9GPYc znxNkN%>jRs&jZpG){}zRowm%4e=~7~J~$K)0u5kCLlsv4beRLZg&l$H41;StN1A9I z6H9e557uZRbAeTUnwi4}hw`id^dqL~zr!BOCtmrB6@6?>b;+{lsK)KwQ$>wC_wVQb z3^?=Jr~Lleag+`g4VP7${1{~QCt3rbx_YdzEmWetzyLa8 z!s#jJk{+G=JfJ1Upwcj#|L@;$_Ifnu8X`+-JrfAcF22v^(7;GYlL&18{q}3Yr^5z; z%I__`(um>EAqHA@gaHB}L^Sq4sS|ojIYsb%6Ue=<7f!01xvg}N8*OIG=IhE1 zR9IX??DyCZFy31*MERX+@(Q7QqS;e{zvGt5&D$hJzDcP6Q@4niK`^7;`KtoE-_Uem zdZc}Ec8X1=mDi|#|016O=N%UD4C}YFN-HRKl7xwdP{D_8`xM7NcgkG<)CEGM>Y`@P z0_OXtZ%t8ddD_@`lzYs43L)pTnu4KvOP}&Bn~+g3b71&f)8=M^+uyy3-DXeW8}b{c zS;q=Ci?f;_^#+C5LY3k5*I6lJ{%CKMe|Yz6qz?!Eofwb8{P4YfLfWKZid*20B}3w`qnaFSUby3b6yg|U3~!VYUp~%h+?>NeWS>r9#c#ndlIpQM%8@ zZCX~clT?+ypgWEF%fIOiWBNF}a_;UsFqwdzgm0OJ@2tZFN4mYeBY+d&!1ergG+Nc! z*m@QYDmNR@>)j_R<{__j8>^uJP?Dk1_MRccT>1Wcj6j4w!h@j;$LvSD-e<Pfn8` z>$zd`uiYNRS@IQ-i2NHl%@E~Vt34_AlN#m_G67C&wjw_ZT_3Wr9Eo~cqjd7?x|18$ zgLsbMJ%;4Z4>zB4W2-C9Rg6d`>e=&0IiCvV*^(sPvPOEdy!SHy#hC-RV*kULqvquN zu(Gm}uSTc;+mh(ndVM(Mxi`ZK0E9KUY1^gJX{p5YUC%j}0kT?D3T!ii{Q#i65y`)%i7!&@!qR(%w(A|8z0FE8aBzG}@0 z&ki=0Fl@QTxgm^7E-#l{!HmnR>v_K?2uPE>yN%x(toEy@HEsqH3rXK9C7p+ipQ`d+ zA>K#$uu)YLhnjofrLVE%-9L+_ogvEqTt#CVhHb3flJI~41|lRq>A!A{I8s!oKg zTxGjBWbrMFSk!cB%@&Yzw&|MB2x@L@Wv#I%h%_P>&i^85nWdR++!%+=%c!kN+|5#( zj#r4pL_>T)(C{gD2lT-`xd^kyN4GQOg}Ijaf~lgS+kUD)2G*;edB(_9ShzE|Bbtvxi_WbQ3SX@6{36~fZa4WcXt`L93JD^LVq+sB;+T;<;3z1d zD+RsCRKlPm(Gsgy%)#4{bMZ$f@fF;CY^v+mKeBPuv~eV@yGu*AH+=fj;%atLW4Zat zH_z?kG&~&1uWv}aznGvmZ5($^p?cJcMI=qbk5aq$pH+L4(Z;%r_&@l-AbG+dWixom z5TY6Ip^VV;YAbbyAJ%@py$gMj%$v1JTAXOtYccnn6b~VV!b+olQ#vBkcY~(1L&jvt z2KN(b(lW-xUjLQ!XEeMMAsB>4f6u-&h)|jNDRI)x0Ar=m;)|fLAf~b7p;d`+ff*J1 z^I|n<5Z6*PvXeZY6rTOl|1UU8`TZJ%@;(fwB5*1yZb#cpjpD&#*4tH78a+#;5p_L* z=^eTi3T$8!?dsTDQ8LS!Qf{hCoT;?;2d~fDKN{S&et(W2NK%Yt)nU7fYh3`V)r2X{ z0v&`NQ4M9C8q&ApPZmKEA7;5F@Xn|4SI2dY6qtVBH8mVaxaPP#xhZ4L*&I+I3#(UQ z1#=Niz+K}gBvBPE`miC@3X+5@9D{^IKeP7Ak85&;+6}}W)|RhFv$;y>tKWG=$jTDQA4m!_P1pilbLFa zrt`yKY;5e03M!4GI^l^@&$-}iHv5~&iMMO~FWSd>abv#2!jUWwAe7h+XY#44`JRCG zJe+6nOdf;uLyYddMbD>6DLyMrCfJViGwxeQ3BL8WW=9!Wqmu+n;|~KiQ@^ghxg-7H~zBO#2 zy8w0xxTYzV*{xl+JbcWKZ$>MbbU)i8uPvlRHCFggr`UWsjQ9>w-S+udRtpm1wIjD(7dZrc>o@ zorcqv=E-(d*v`4Lf%jRO-aw##ac`qg$eK=-inoh5ieuCq@1TRy3wmhn)4pS~9N3~E48XM+eye^@Z)ag<|s09Q|Zr8reB*hdp^^C3Q( z6wjemVUW>BVI#wcc6A`i-eseu4nJvXP*BR$!?#m=O*H@38D;57n8v) z0%1U*;A19akjHL#R=!^T=gqN;ZQh8750y!)246D6IF1D25CmZmM&EiBMvC&N1xVM- zr_u@QyUqVN3l;kiFu7S$R|WhN%jPP|lp}-RRJW6(hMBjG?<&-RRp6JG6;P+3enp()eJ-9GYV)^i4PE~F1n)Sw1XQ0A$r}Z98Y4$8>3VQbN9F8O+4$0T{sH?aL-fn8%Jca*=L|nGnj1q$e zPRZAZNShq(85}jGm0aUDkZ=|HhW$9v?F8bnD*6FnH%T0Z+5ss5mtw5vT#W98r`dSZ zR@T|5Csc~}>+2rjQi=1M#+5C8)g{m+iA? zFy)(|@L*LSHLJAJ_6U3)F}hbWo#^E;cGxP;!XU^FuM~}fA2YgAy3UwK@u^9!(ZVCv()WvnM&0}YH65sqBAH3xQCgvTDJGcWOlwUZv{yvkO=vD6^O9cNh6iLTlBz1P&%<|In?_sgrYz9XyOYOS)= zBAz2zW`LVy^f@fxNS$00u!g&S^grW7MiM z)$x6i|3z&r{~3ycnh8ab?TrSq0s|gav~}HTZz@?tV3*g;;zwAQHM-yt7COMdFY$rgh!O zP2QQ!o!4$o>G>~P9;v$!1T?rdgR|ZAG~#^CgDW*P+W5~Aru6mbb8D*FeOxyv^2ZFg z+$4X5Q^*&mqK?kKqLlXm-x6>~e_Z!La)0XgO*;ztts#a| z{u8PWG7l9(Hssq;TG(Tk-s$0CmfU>xv)Pg9_X5Nr{o3#Qa>(^h`lK;CelRwbOM(v; z8{&-wS}LwiZ-2Ivt%&Qf? zXi2BrKXyH;k^n*QW=Po(;ND232^QW-_ncbL^MLex z(Da??Oo~n3o{im8s&YXrc%T1rNcSe-#mn=a;Zh~N)y8X^F~c_=GMI^~nYdGVT51!+ z^=??b{fnDfbm{_| zS=-5-+YNftO@V$|Q?W)(?VCsQG0Y!Y`C{LUBj17)QKWR$FZ@saLwE>>rE$3iYvaxa zZtvputGYB!HblHo%c)Iw7>e)=tNnafHV7w-wIpn2dfj-xOkXI>RLl)Zl&E4ZFT$*b zgX^@=nuLH}?Y_2s!+M**C6^+z<>ke*&ImlopXw&l3dE8JLywXo!ou?o^$$({ql~;$ z#a2k#`Eh!3sSxrfF?-cO@HL%?^pmZIkLJ zqGy3*t=g}9rh;SQJL(%3EsI395Rcncp+v%Qx2Q`uMgRX<3W6#j|GYbUEwE=>`nB4Uw2~BtNcnLuH}m&g~4b$D)_j z`INHuSiDXA4(k9udP_VHHCF$qbGttDG4RCX91d~zaj2GWT8_jPU_xkK6&%~Qd z=4W;u3MZ(ObUZ`SlZ2~LvnnX5oo7=;R>aLHHy8$|?FPC- zJrU4W%f8v&6EYeGI?3iqFV&kB+I`lJ^F)BfZ_^$|fK7@Y3y4YXep>}u?Z9S2?;S)` z(|}r9#iJZcn?zElv1qWHClzaMos^$^fTiCTeQBbQH&!k{;JAglycF&ZhcO!#D|z7|>8Y!UlC*MVg-k zqw3*Dm(A<0{GmSZu`L-vEbX`~+uhNDZJu~>FL4u+?nc_L6#TvTv!WdoqmN5y#wSJK zoqX-P*yq2I9x){eqhkp;1wu7(k~~*8#$%cr1G! z#3v<2i#6fa$?85f8S8%udbAJ?aG!2!cD2}qrdcfZ~-fJkz9a_E>T6*^PYQD(gn z12>1jJ+G}_KE)8^q!Czi^9*XA79a3hMe9Gfy1 zF%??&!a~|0Wm7h=s6I^6!{UzZ` zk{!#0FU4_zb0p|_~I+KhOxzk${vMk$-`VWg0Y9KpFy$| z09XP-1Pf3E$eIQMmA$>TO=5M&&Ci!x4OC%V%8twIKyhi3D6_(FC-D6Z$(WJ*`Sqw} z&sUbpsV3t55Gr6c(rl`(@4>uTRo5fYJcY$k-F?FdZ8H!`;$aaFiWd;Fh(3uxT(qt2 z)J|D~ofJQ;#`IaNUM*WO-=OEXo5XOVu_}$JvupWd<+!9tnpoCqF-MX8u*RBr)T@ol zvV&X%?Bwc0Z+OE9nGjKE)R?&>vgn?qKOZj`)R}^BdT-b+&*s*j-f}N@%^r>*yj z)I^Tj$70DKJQ0B)%Ay7&Y-w}Dj`CVf1A#QE<%a`y9>V5Orw)(JuW;G=g#xyQ#eumy zAD=pTK~PxQMp@nHhDzhN@;LJy7h1USnSd(Hmv7Nri}VShWy!_a6s0grA{beiqR)Pd z+c?CNR##pN7n;-&xMtWt;;PRtpVdQ{?xy1$l(uu9z%)l@8~_RbD3$~4jXe5v7iy7K z(Q9(65rI2x8eK7ZSv!$YOQ!6Xz{cLcPtEbFMj2P*_AR_Zrrq@fZX8HQ1o@J7gAcf~h(Zhb~+8gMI^aO>AT^`eOV+n+8*J39w zF}vL=Cqd^$ezxMEEZxxCY|~=xO7z%+)p;#-&}186P6S(Jkh7_+3tme|bPaoRd5m)K zGT0v-w$l)-DvkZX2XaGv7!iWv8XpWL&*l&4M8zVs%l1h=HxnkxD8b=Od@3gKu0%CO z)?L@+x(Kz;y_Td*Y!42QiKQ~u27R#Vbzpro ziZaE#R}nRQ_h=>RD}AX^l^PL8t$t{jLHCXQzI7{{H8uuDJk&*ZK98*iIA~fHVpc&g zu@*J;w+DQ<*MkYTX_&H-yy@j}ylOJvNPZ`i^xnf6E2iKdb=oyO8!8=C;4n)&YNNwwyG!?aZXLXUS z7IGlFd4B!eDt{7O-iAME`>g*QY0WlxyF?GzT%Mk@z0f_$ju&CAC~FF zV}8SWM+`|E>)n#hOyb|FObv^XIZChUjbkk2((i>%KX@!zDWSB98GBeKB!5;QYkX^! z@(NCYPB5l>{B!iwtNW;;k`XGm!&NhQZBdjE9OpN*&!4@9&wJY_i0xm|aIfpo{9%-2EU!C??Gyc?ye+>gPmV@$$Wcr{&6B6r<*kB=tvd8B zJWVn7lg-IQgd*TaG1QdPo2tE=2x7rj09FH}S6;#fjIr9%4xY!S>yVs*=52&sB2KA(oBGcjgcnifuN!vcv6vELTgU@iSxZBm zQ&ch`y^{Ql?pM9bgBlS<^(8tSq37cZ=AnBsglhe2(PintPT4iEreB{f&3ERfuyd}8iTBFZ!=j5uZk2rdP)eUfFuAQqz`E-HBpg^->!eq1$@5gN4r znEEq>F^X})N?-oHp=|wPP&_33k*@Xey_oOjmPcjz*UPcjL2rSiI1HS&5X|a0tpyna z971DwtJnKzJf)v`P9m4TFgFF?#I9&3VSbZj1c{GjqsQiz-o|5T@pNeOA0sJKxa2wa zT7KKb>?4*ty^@hHv`&bEHjM5vR3ba~Bx;N)^xZjq$vtGCC6-Ymrzu8~Z7Oa`jiS4=#l89d2x4^#H4zoxS zVLco^AVj-c#5BO-q|bc;Di=?yA0rXn6kjk`q;0FJhGVbdn4!;6zVGPYgH7qejYV`O zF&V`e>_k6c+_Pqmk@LM$Ob}r`f^`2pjJZk%G|RAsMygtZE|(x^=i?#sX`ed23K#Ht z_b!ocJyj$Z7Qw^V;ZWH!kn!tF>fjJ~9P1aqPbhFMsgMLyxM7jS09~9HS*C1ZDYVYG zV<__5h9!)>|tbo@wK-xb7picTBb3U2T$V##BF763xnu`d6F(< zE61+5Q?vX-cW1B@t-iv7k$$pR&Y?K%8oQY1vx;w?IgD^163gQ*WVNMrH0*Lom$_=u zPbQ6-S`7jCtRyGLOq<;CxMhh{XptcY>&sHeJ>sF|qzQ~=-qNASO_;}Z=&N9){AjdFbK>}99N&5{ zC%SSME;u=9e9Yg=kC9s5?XE^}dRBH(GQ4=yr{a8Uv$^J>2YRO;SxFbg2vUk8oR=f` zlh_3+fs%m9!TGeNM6z6r5CW$(`dB_#zw+^m5ym7$I^cw->>?me&n-4rwth-IK;Gvn z=8Ia}_2+1PR9;p3X9=5Q*i1CK3i|GH(>_!rLT-oiE4M_S)I*GW#^ykj`WmJ7>XtHG z;6x`jG0Dyjj8tCZTRMTek2GYFmZaR~EW!*=ILxyZo(jhLrqvR7Xfxc7zaXb$-$IX; zP3UJO-#jwF4YW;QiRK|Gz9vfwU)$z=7yY)fx1#4W#F+!jH+OwuMeJY=ctq_<@?R0j zhz4z#d1L!tQCzGPI*nN^;gaf>lJuF<#nGE&UHq^+z4)&aYmG3dLGN)T=4>=smvmky zQ=2yZ{7Js)tSGx%81=T2nHYk59QEvZGVXDvbegrsiJrzJh=GD$m?e2aAlERBl-o`+ z{YPb-r=)VB`sOE;C)4<39Z8<7qZ4G999Pni~+f^7ekBs=98jm8hr-zRG;1( zY`IribW{@%uMm&%BLR0{X^9-SBl&GGl7yO>`Rz4E52&k~o8^&<)j~#HG;e<@6EX>+ z8!MMezP*s^qZgg~BXPl22G|GFmwHbn8R*?r|XK;b;3>Qw+aVw{b0S@l-kZr-2I-M(}8=GKzTWq-hUFZgEY=sS$QY zebM^g`S&MvU<1^_&h2*C3H>{`^{rr)1naoh*Pb7!pujL%NR?##d;zj);vmHgu6swd zqt{9VJAsdo>Ry<1pK|0}>wy2sZMq*T@6VCYu30=OZ(gAJOiT9hw z103}+hLfx3Sfo^GY-)P|-U2y;?({T7GvxQG)(OT|&Fvf_>;JJxPe#cT=4U=U=QJYR zj+bW$e5^(7rl0+wyEdWP>o90$2NX9`UaW6oQTAYkHCS$8A`VQaU=eM~tRJLiXL4lV zJr(@{{kptnAl@$@*G(O1@w1S#nCONv{}D#?bA|Xaig)MyO>xZm#$lU@U#>nyzr{xT zM%?vQOVZvEvSUzyr<*1pY!oBriZAw38lSjhys@1V@3-2=IP3_ ztAMT$F$f`4cls4UUmstZ;syiQ)!P4gV}V~&aOA#ulPjgd$6WEs@msC%3!o*M|G-|> z;vtMkROd_NS&@DprW6UmKmHLo5_%uuCQI{P|Aojvgn)?@plUJ4J~l0w7}4xtj{2d! zfB{FYejQ&dFMDVSif0z9X|2TdHYnaziDHhZAcZ1Dx>J)qbm67pi1RrmN{a^FOXt9q z-p@|5y1e0AXV%{=4zz4pQTik?;#`Wwawi;Q8=LSoho*G#s{G$kc#p)@o@Li4i00i@ zPZrBGx*waN5+&7!V*ev7U~%u^(4nu-I@ih%&?7uqwd%h|=-TJW)k}vF$&7!`7?he( zAL=vOx9DAvar}BcviOV6js!$|(10gOn}xzv)MB?zQ9$6Huq(C?rEz@B8r?Ui!yVdX z<8dX|=a1h{l-&`edT_3QA8Y%pCQAdRd_IG*LTyYTThcJysa{W(XtyBEg%mht1bF`=%@WEC}`B*;D5H>H1{>C*RoQ z!U3a6g!BRZL+!9)La%&a+|BitD05v??Dxv)$=fq0@h180G#8rh?+yl(<1RMnvL;<^ z2hVpl{n7bI?pDz z6k%rE+Qz8NUEf z@NEh~cA#xb_nu18{n52x-l6eUm#9Oxd$CQ|OKj)kg}2=#V?>EH`GjOp2HkN@zT}-f z%@4Rr*kUM%KltsNV@|L{r#fd~;?}yJHFcz!m>xdath71;^~=9K+8-<12$&6H{FcBu z_M?o_uKsMF+wkKM%x0wuc^ns4BLRS@>4b0Td}FpaLDTVeNsJiH==-wgTkhFhT>X-{ zxV931#j}CEbv)-#8&N;Sl`D_DBDCl4F7x>kQMp0BSH6uMH9gY58B;=h<%s&wco#a1lO z8|<8wLl_hn`yS7dzj4tb=~(<&G@2R>ZvkEX3(g_rJ3xcjBFy>*4=9dih7AJOy6>C^i>H-GtEQ zX54wUVhlUQ@c*r>Eu6?agI{FC5G2ZsJU^%w{nc@dGq<iC0okr1_{(vZeKj}8 zoU2*<%U%n|5Lfu(k@dcm_-CasXF*8f@@0D;AU7)jS?0a6wouKbgzMCD|a zg4L{%iFf~g$PuWZ*E>v@Fbv$jiCdA5S)pZs#4FSEfN;gB9$4Jy-kGmG{(Fhb_+HtT zP9+omSuB)ZbZU3`RtjlTi!v`)Swk1Q=bdFwRTmpEEJlPe60FN|xj1bpV#?kwe-t71 zPW&<)JqHWghx_MD!FY|6!q_Kqr!?Ks@oVFHJOe)B;UldQL!r9E%AJ1w?>b;NB=Y;g zY_m==JY0oBU_=|u!TzJSH@pjUL2nDd7*x;r#ysWWyaIjtHH!NSE25jbSz*d-dP@2Lp!|(+cp;-#$IJ)#YW{ z*mIXX!t_Qt|6JCia4Dr0>sL{EP0lA9nUh3G!{z4!f(hK>{!Rau?{6*G34sCVX~zVU zK-n(}@qZ2q4BVnDOd7QPMV{>c?3n@d zXp8(|(+pRb66piRhYB04drsX*uFIV*Zb+d+Kw7OU3=Hpxl&FwW=)fHF{q&Ts!RU|s zuD7-ZIt6Ob7fpPXZ(?a6^P10)(Z5T4TxyZnt_W|*xo;edIUv}*N~YKv3#Eay58wAB+l8=*%jyK=f5Zr=SLd8ch@ojA&In0Tufq7!k=~p=b7GM)MLDdof&V>90WVKH}r$*?_E6pdN-> znq*RTSFY#X0QR|IhVh~t*vm6iE}8!!RpRhuEWhUg#`3<}fV;=CNVErp?QoCl`m04w zk@z*qNAh1iw>pJOU?@ZJ_Wl5UrT*jRsHK_fiumoV#9(EG#9%F7W6CAjGisC(90J3! zLQB5F3%A<|Gw$OhHDGN%o{63;EsVCGWVBomN-c65v^F8If66P_&w14I2Jf86nyo=9q` zGke6I1WB)Sq~JzdG?q3SW9FRWt3X3dYsOj{P)9~POo#U*G1?wO5K`{lUWfl!k|ld} z1YTo|zFgAPBn7A__#j|TuiTqPmau|26OmZy%IvLld|4`RYkS033x#^iUVsia!G=wj zG9;Y9Zlnm=@Lvl0#sX!`zTNwY==9a|OJ%K#BZptdevZGX5^MT;+chmsDu;(f7613k z0QD(LrFKnm%yWWdcf(sOcYj=-vd9%=&f8mN@0;UWvGH%SSW4CJAueZ;ct-b2b0H0p z7tDaIcbR$BvGHWT%;DxZ9%afUJtMn$9CzjuH_nE>rkW6{0>iBv408_(h4w~4Uc;uJ z3e;9}IFjy~zc63F|Dwt3r`_OX4Hf5< zUCNkkeH3hkh1F5BAYkuyy2!mV_kvCYjnlo=z(T+TL5#DbUikmi(xG zjrL%`y_~e@_`^17@rYy=4Pc2eAZY@$RG?ikW4LgNewF~>a#y>dr*wLwuq(=c6W%D_ zT}IhrjCP`^JXo!B=z#mD>ump_czUA1~B#lsWPYw{4%30Y)s5m91bMB z<7K3?Bfc2->ra6>`oE56*5qR|s2RF%7Me$tl?QONU1Oi_A4?S{^3`$sY zt{ok#AjMe04fM3&s0OVtd;wo==XTZ{|9+usz;RC;^-U-2&nsITT+}Hw3sdW|kCJ?( zBoQ)vp;u~r2xqYv8Sz#n6a7G-Do9T{LWS<5xm?}ksbAMl@=p4PH$mp;?}b8BuHBFe zhWM|;`0qbvX5wNK3Pwg=O7cS89}uOp5 z4~~$7D{+!%+GJ%r!X8BYUxY*QF&ao^$kBBp{0qzd^Npk&;CS<;2)6zW^Zt43yA*&y zDRK4wPI~L39P}`WVEyHzhKU9$YX{H zM2&%niT{JDfFJXLHL$W7uWA0@x7Y!EH2c$A_J2hIz*JxjNlBH~4FBgX*kk~1nSU0d z{BIly+?t>Xum(A~*?%Wl{&l~ePa7ZFLo8X(c^|Npni%T9QCvFUBS^_oi5#^HkN&iac{FpI9P?m$X=JJ~3>J$hsm z(KcRTz@J#TZ#__eYE2Rw@%OHFOCm5EW(Rjv{n=AaiH|3~=|f3hPl)+u?+}$edPX~p zhK}`#dGKidWJ!K$1KYFcYH6+iq$2?DIy7k_m#V%-*?ytkS;M(og+Z&d8tTCl5BMjM za7WXu>Kq=cFJE}CudWjKLhuH4a$CQ_0PW(HMqs^lt$L@27uOY3hLGs!0D1&kO2cTD=k^DXyAHX!g19S>xCyUg^XHy>=h+TY2w3_6v zQ8sgcz%g3M+$>8oSu@LysM+d}Iq-R-RV18L7?XkIOz@XS8>aN4gm%k zJCruoNDQ%jB0Fh=XZR>s1yVI?o-aItdp6bU(XW4T#3#JK^XePuhkd@w%!N|ElMGE_ z4Mr26RW`n>F++D?yvsd!g3A7Zd5?q5pzL#5oaSSe5~~EC%VAHAM7Db;zZ*^4#X z@eFk|`)nJ(r2Oh`Ckc$~*)Sq5;}VZ2-eU#0IZN>xA9BuoVASKSJpOBi!R~6=qn?v* zag!pJ<7UlrXV`0PQA{MDi9sC$xMW7CC^=*CnSy+?j(~ErmK+^+%e=UR5 zeOsw@Xi25P2BobVSl}Uvdp$TZI$znxu-`wO$5nv|(}DJp)$QlIqxGLT!KyonRN9UO zM4f99f2*GRlj?bdTl@W1JqNho{T5q0(OUz?spzmE;qCE2TW*zo#)ape}a_Y;FRMdGX%Kyi~r z)1t@Lfx=3&TzbpU@s`W?wraYcj~j8$cAjG?Ymh%~4p_}T9%0n?X{l5ZaXATZq^_L; z18&oP7ScHwJzCFJy8AQaPM*4JOEXU@7A>`k9_KP^UM&I7GK2E-eH{isG`-18nSLqe zP=z5~W%m?8`_gTcyY3J3b%q1nA8r8uS4%cXYl5dyM$y`{Y-{hNSkjv)Ww zIH{>xGSAfy$jS@U_A7Go)pjb{O|+AaSl*el3zFDEuu4bT{`FgWO{PSHfj3~~LV@g) zK^w;=n)`6}I+j6I7^lHugZq_>y|1)?*Dl@8A zHoPA5AnKQ90+DQlGa>aUrQzg7Y<1wK1DQ%4c<$y;H%N=r;Y-RW+ zsn~*;tn|9MlRB1orA43-rvfQpJmZv1DWxknjmzH4U86RsL=#n6SEI)AI{sn7UXqCH z02Qg*pkyr87q?NdYy6GGSUVd{!ERDb5m-m=I-tIK4{YPs@^JS272&wX!Pq0May93D zodf0d?QMS1XyXC^rDG>y)7KY!s{#VAbjnSVocFE9Bz6m!nt?X2qKmypHVYO1f~f*o zk>_q1r7Tw{~CL5xQQjH~|d?NlN_994;T5Xd^Em#;ZpNy)a{PJ_)Wa{GU=lAPM z-zN0g(*t}_%?`vo|0g;MIWR@u8`#>y8|1xR2B%%UEG_bCfbx%oyEsV3^* zJ{KKL<2rs3^o>uC(_%j_|LpyUM88Z^Z@Be$GKlJG)>n(7`cXxoYJ>zyJu*rKx_MlV zCx{CI>{uQ?&3=hZI~ia!If(A*YFtv_8$4vOaSrM_z%L!zwq|!2o3O7jnd_#Lr2r%3 zRBO!psr65bH6TB)FHd8K+TV8GL|~T!_ZCFk<@E&- z(A0q*n@uj1R_Mv4lqz=x^d{0ye+f8u`ZV*2cIb;|G_v9Bik|Y@KiRWoeE=mj?SEah z`3)8p5JDh50aS9idilyVG(bDf*h+p;$LE}ajs6Y1AswkvF_q*HoY>=--VeGeMT#m% z%%#A~VgQ@oU@?wFvZ4A&bq*AlL-75Wkhx>PH@|%Vmcg1YhtuMSnCmIa#{(3ryutG1Nci2-k`Z{T#-O>N(8>XS~zccDDBvVCLs zT4L`8z)h28YD(yNEvn|ggO3bH`${jB#@6@sCf__*HV(Aw(-4_621;Kp7V@$yck=H_)*o=QXYo4QMyBZgyv0(AnO7Ub&EryRxG*Y3*f}J6ZUnT* z#C$YgXqJk)InbBXaIjc_I(Xbpgzplr)L1|&aL@bw&pe8un9!)>p_|nIr@im~ieg*Z z21EoABq&JEL89c~5R^1z$(fNPl0+DgI1H#H$vF>4(tsq%IjH0)IR}9u!${8XHG1xO z@44sx0pAaItzNTcdb)e>?%sP>)l*MZaX_%hDB6pY@gIZV^S^V-L{4xGE54oZ(6sjnx&h1-sH!bDM+XX! zq&>ZF)sN_<_88Zt;SbEm)~^LH3^}#SB(x4VF9XzM|HlVg`;7-nf7%khfFD@Pj&b}? z35$!l^T`()_-pxF<3??Ub1~C zq9F&1{4_EAk$LoJ05-%061|{rJZ+yI2K{H?h_X zlLb~1hIO7HHHZY9#I}rNfmE~H)P`nm0kNN%qO{*Q7a(l|N-kCgmgfcDqLDVfE$5=8B?C*}nne~x*LfB0_yFnl z5Ngv0Ucw3%0sp#L0qkhPb*E+aSb*erttI>2BYHVd%TPGA!|7^?MyA6vsypM@ZZ(|C z^BOkUA=ZWM^Vf+jaG*r#O3NnvFEn#t6QAuQ@AGIjr}EBrh1IRZs5nJ)d+9EPgV2xPR7uK;WAB7kp;qK@m4K|@*^n< z@HL6{yeu&Tv%3)hye#bsKJSSBDXXNVNq_#3q6~Og>H6l>gigXUw$AZH3k(LIHsF?1 zUoWxz7?>0(?rRG|BPe0W<+O0Sx8&9EbHfgxxX#7S(VgwDh6;EHTtZ3aFYtl}x6yIt zj%XD$qXi$pA)tYc)}CKfX7iksJzRVx*Ww?j@a)+$Uvc}9Qi9=%i*63?r(XNVZrgfk z-Djv;9+Ub{wU4_G3~qsyI`NiHEerCnJxSEdD?2*shHFMb%HxnJ#3_#)s&8Lr9c@%R zYI=Qn`zDT6m8dDY{ZFH%4lh*<8sIhGk`0bv@u>vZ5!K?qU!0&kJ#@K;dFTuCSc`@A zRz;a!pzH;=K6`q@OlKdthHW{ZFloBD2*v?m6%dGNxJ44No$n}V$F*Eij_t-_n(^6a zBVFK3g!1rE|8)PGBX~KQTcZ@WU)?K3xM=DLxq-_E#j0gClI6$>$(l#%ov@0cTT{PR zb&3eUvTmzeslH|l1DgoZI1uXLaO{=x$);OAMkJ~_VQU4_q|o|--1!>?t;+p&X39qp zg(YC&f6D{b^~fJX;<%#qW#KKYs^c1_bOKzkvZT6*i@Cp`3vzNkL*^ z&0{)CPP?&uC5ZY3*yA2cWR&}Z^sT8l+tH$P`V`b|jnsn^@q_g}?2E?NLR&L1uxm)X zl0O`Fn58t&G!r+0ius83yQ@Txh?*0%Zrox1FL;4ka~vNsXf(xnTlAMGz7_<3ZOy8H ze3-!|8Tp$-=$Fl&l?ptLu8AMuKb!cM^?Chd0H>%cDOmd#5Ao0Y{O!hzbOzMZht`2i z|Ln-LEP#Wk(rXa>6Ex_$w*OOU!vBe@ye0*fF%dsmp})2AtF4D@^KlD;!otER+1Jef zyygd708h_e6&35SYt7?)rl~@D>pu|~X&hDn7>`@XKN5FJ7K6UHxwt)3_b}k?jemAN zTA^8oVA*v+ke*(e7$^?@Vi%kt_+J{uo33_}p~)9AaOT_vF3O}p*h&uqrEX8x#2C}E z0|@Hx$Mn}{qzcx8P=o76yf_CtJA0Bs$_KAB(cWYsP8&bZlU>xkCtsfY=P4LieWcYc zYtbBBrdk7*5BZ8okys4y%kx+kWwH<9;W^j!WL%Zz=O6s@y`5p3o14cjM@xf!e0;QX zpD~>Iqs!Xa5L=Q~gQ!=2F5A;$g98~r@c;!sKfl}hwe|FZzbEWp;|0K5aWS2+{%1QF*v0@-x6UX{P56)TH7^d# zpa0zw?*HD>??G5Z`oDn(NVoq_=2DUgND@ZlvqGpLi>;x3Ps2EMtJ1pjX{->Z#_P{p z7ahORW&nWHD3KL8ae}c0uqO{ya)t0}2wy74zoVP+Ii9w~C*J+F+DQ7Xf_-?GS%fDK z>q_p@t#9<-)%OwUBYECLyNL;i?2ZQu`*r9bGA{bt1g^9xxWhTB!L#n~a-}g>B4iYQ zFL?>LTw+i5cgQkK^kVc>8{W!hQtf2cfp^1YY_1RJc!q7hO}w>qw9o)2k(+hC9F+Sb z-bPxxiB03V>94DRaWK=}D=lNyA#&;n!Qk#PYpvN=#ChmO<>3;Gu8roXS4>22E?64| z;G%^l4gSukz9(q!@9CbPsR@UJOh!O+zojrw=B4OWNPijz$nDWO#(|?k<2=mvYvJvi zKvLaxcYIEPXr?iK7yn+=WC5LqKiNZ|wi3-l=helJkO6f8a_CBz`}(2FElRN$j{xsF zk60cozg`b0VyGnO{yC4T_;GIIPppCbVv-Z^RY18}I&7DwP`|csI+3>jO$Iq+&1_ap zIr!T8%O{zAw}vFL}^0oq?4!`cF}`99DfYKoSu83u4h$ zI`kAbt2p&CqEduIrF>N*qkdvqD;-GtEGh6_?L6U_ct2CaQ8Q-Qm72yJI(H3(uC+tT z#;d$9xOR_@v^+MhX65M`9?#Y{X|I_JZXx@7r@+^u##aW5?Rduhgj|;IoaH->Za$U9 zJJapl&PCVhdJ46ODv!8O|M{2RR+OyXQcM?2H1iGF>)Gi|t@ZjPBf`FB(J)(v( zBlOx3t0OX$*EOLx@riaNmqv>4_NM`qr`m4!_rAijo&&*G@#hH)dtYePQYAfRGYW-l zb<_qZ?$Vw2FI2l<*1Db!-we{i=NJcSa>&t(hv%+1AK_`$oQ9DH;S{hW5B2SF9bS07 zC!_;=3j-1BQ5i9_b>vn%sj*56cXo)gOcw>D7KGb1e6P(mUjgh_sceQ~eJ~)ty2gSQ zj(LZHV{AN$tU9}AO>4!5@9|?GiW7XDH^@$}IDf^m&sgl( zX2?8b6>nQOc^DQ2au`J?M8nrx#2ThhLWsFOrhPTY0q%CuYbGn+TZ99!UA%#8Wx|l> zNNYTo?ZK`E?CUHtwFI=49p8R={TUxQQw`*VY&-5vXUYoPt!*+L#=RD(YagURa7<&# zwtL4NGXxNUDE4W8p08?^Y^<#E2h@26jW=MAEU62x?5KicSkHB_<)*{4_N;ja=JEmZ zt~ydP#8X{skgTT!2dFx3B(O;z)2W%fIwkqK(FeWBG8&;F-v^?viDO)h_H&>BLs$Rel)< zFH&bzdH424F5nZ>E_Y{e-QHW_89MCsT5Z`Wv#qmA3D9O__V_pjSVQuq&9q#YXj0_W z`JzEam1D|VuA4dGRom-v&qh>#^sM2#08r_DPK_YLpHkI~avpu}Et9dQ#y`J#>;|jO zt(69NED>FL>~aK@V=m&9Nuh86xW7ds;T_FmQmusPH<1f2dCGCD;PERacPs=gNJ;lOfOUC){G(w!V#2dbp1LQ*rcs>C@d5YG>+ z8N#N_`XZfAaqZvQDeqN2na3m^cbeE^lXQ{T3epP3JpouO(=(e7OqQ)ny(fxI^GSqk z4LMusnZLIu-%nW`kJ7qFPSp)(yPdz{naAInR;}efu zqA?kRp9~85=&h9S;KZD+i+nQc*xAlH3f3ZyKvc4e$caI}t5JwbELEtBece(D!JKjq za(n?w)W zC+RoS&+MQAKW{PXltR`j>RNueA*(m+k8 z%V&wyqzc+A@vxp*h?s6b=|xR=ovJmxGWE#@0Xg@KgVs0qXO(PQEjt3kxt@B-M|I0=&#?qYa~{Qc%0B10f^k>XDM$0FpMu&BHYq}0u8TB{+sC*ESp&W%RogdAJBO@e zZ_ps=gM7)NTHkS!9L+hng1S~QkK^F?FA(M`&)8d-M6a1JDWK~%F(+Dd<*<&7@}rn{ z_P4ccrkoBv8^14ZSqf=^fU1J~8k%Mt6+hEu@CX_EHIlwI$1A3YSFNWozs)Y$1ek6* z6ep3mKo|`;5LwZbXx7CWcVp`Mv(Glv>cW7 z(M8qL}ZMb)uwH-IFW>aC38nXB*vuLY%ZDjaIi2~P$SB6#Z;(1QG8T$v? zkaRv4I}t|9b(647b;Hm0{Hhl_0+XeVd@zAUQr)rOiaS*N@k^N)zZ?eBs?q_n-p&0F zGErSLDrMxMB2pm{5JQEAQ;-?gf{S6mlhCW}8{%FT_%xt0Lkac3I~$HWq4*7y@&H3t zU<}BIYj8TB=Ctgf*iq(uQRLyW(1mk~-u0ZUcZ}-N@;~;Ns8P1-qK)ibcwm36wMp9& zxi6+ka_|K27?T`Xeoai8CaC1S-))dSDeMt1Nw-5oBj)l|zsJGGLkdzmz6;xbNc;B9 z()rvv&Ea%!q2SR`@N$ILZvEz91I=TPf<_F(3UGZsO|pdgSdp;^Ix6CQ)83g=O`uZJ z9=|3QKq8;nN{8#44`j&q+75+68vl6eznAZw^h_TMqu1!vfutX~BfZwH#cX_0mAu_p9tBL`g0w4PjyXJ=Cu#Gz z{U(GmWPpK|{?#30%aRxo%jwOpsF=JpCfD9YV=MZT0*^%!NDv8OG*5G1qDxgG0K-Bx zcDDPHrDl87(5olsi^E8`TZ7`Rt6e8n_miK?X}{H5d~9aiG;D8y$$}Her=9J_#zqdK zHbKBO!3R+AruBRB_N^fe%DnqtgVg0RZEJTg+%Xz<+seq=yZ3QPe#+R7mfs|!ui@f06!Qn_tb?iE^di@!;E2r~Ei!^;K+OXblStpt8RL5APX@;ZdZeT@ znV!dC)WT8g+M8JcjMa6>_Hsw=K;d!}_)84@IQBMK7c!X%y#lzv<-yQRj8vaUFz-%%4HCq}T`^`eg8g}J*%iIOF zBQfW~v9!V1e3^hUkn#z-Bxe@`S8*oHbhxmNSXbgSc~EfHeb;?M%t8ZNYZhZjJyDY3 zVMIbQGKP>C{3I!%UE&fw1F-CMzaT;%A8t!pPM4=hHqS6-8I=eV&%rMbMN|>T&}g}O z3VDDM>FY%L%WMO(j&7{z#RYax!QM0pAD>vJY1Kv@EFicl?VF9_7qm@%m~Csua}MI6 zeo|vq_^1xz*epssQGTmYcG*qUr#RP8cAn}Vrr*|;egdSuUS@R4s6$jyRUsuvrtZyV z)ofhGR-GGqpwHoqD?Cm*qq5*d8i(Lv%F&vSfWGm3o?~q#d}5(NC%`=jx7fI%zVMTP z3Ia7Bdj~pUv-gzcXQYaoxnR!h%%ee?I;XZ~ZLd?wp6=LE@SBX(Su~1iogo{}eB+a8IoDGY$k|T&l#4hocM9JIB$y0ote2p&|!B^YjvgGcB zntHwc9N$xiS5{9px=!R_`virCm0#jG1}{ngF#IFoEl=uig7B3cvVs#M4xMoJ$ zyz^D(Xju2sPc^*TUK+qTW5d!HR8f;@o$4}B5#Cm3GxKIcE6>4WOsy}lO?QjZpx&+> zioue^B7-$%rLCL|vFBJQGzJuX{LiV9t!Zrbv!w_**F|~Nij$F*sp1#;dAeM;se~>6 zsC4?2NZQ^%2#{5%loF%)BUTfmw77NwqK5fwCE<5Iz}tV%le?!L!qyg^+-A~x2e9xT z`<=flvnF|@$Qn^qsm0bm@V-QVyOhpm=DYdSYykF8+=n{D^+vTS`U;)Hk5NAuezcoi z>$>sHGVyyUURiwEQ1>iDB!|Q3*WDx^lqNgu3o%RKmgg$L>HfRm(Fww;Z2WOAM;K!1 zTW0-(hWz zl8LU@Ry^JWf-;geO5!5(UYUBWF&Q?P$J7d++`0hI&~kVd)qp~#XL(%(9L1HuOY=a+ zOdAc$$t$RU0Ua%5x6E$-1}Q5DMi@ge{#U8=oB@vpFU+WvR*e9ujvO z4SOY5?#|(7D#}6})TNFRZC!Vi1H4aM;9?gOcAO)Wr3 zkbz1WBdY1t9LI~keFpt(?{W~O^nGq>Szi`T4SLROpss8o+5E)(=l6t{<3VFREK6ql z##L24Qws#+weVmMBcDDAS}HIQOl8*xjg92hnd_$j_^W%H60XBIn>YMI)&7_@&L~U( z$$az@%Md$f`x2vDTKOK` zN>YHEdqi%z6)&+{8aRMVn`<_@2vQeSz=66@%W)lOz(l20YfE;UGO-oCqEG5l-t;-k z?s0=$fr0vYhS6#U?QDk0NW*d;AETxwC4b(1_Y|@ogBKb{E2OiT028aSWABgvy_dQ? ze`zGa-C+HyE*N#p-Tv*X_9VyNXy>#uVVJ}Rp$%Opec8(|K=c&ONWnKq`L+dr)EPNx zAS-c>^Z15ilQdJ?9abrL`+#${y$Rt#gXM@Iz8S~KsBXTg&PD6z%13JZM3%n z#o)oyWq(*aQvP+@QivhylQ-nosubGXfdg>VFr-Q)3pSE!W3=p&oEi4Jqgy&BwrGrM z3_Vi(RyX6wi+yF6TrI;=#y(Df2)o%;vgwM70%MV>yhBdkYE7)`Lh6{iG>b zi4qf5f1K6p<&>Y??DAAeREhx{nyM0*Ck6xG^yL8Q+4!QQ0e9H16?!7pLrfj_M=RPSGJ^rfDN& z+T;>vO%Jz-*BAF_a(w>FcPd2L{C73SklScNcAeS>h12^?u-1VdSy98`)|G@#ABcCw zEr-c6U(H(#W-W6^_Val2|`3LJW&=V*;J<+m=<7AM< zLdRR6xr;pW84ufsp~+vfyUI}s7A6fHb~amD%{B#-x9Cq19z#c@Lh z)l@qGhbWi;i`yN$*(+WGGUGhCqk5wL<;LcqT|I76!f863d=LXc#S2)U37!oQtp0}G z<+5`7i@VHzJE;{MTraLB#RDfnld%O%KfCbz5@MvZV`^4_Q|KeD zgHcfgHRpcE@>XZZ(;*7{|-;f#8LetlaN%L)uR4hjNu=(F>7Du=jJ#aIlw>f-o87CdBc zV3lf^h3DkgrLiDsO>}EDJB)oNPL%aD4lQ4FE)=`fSaEN~iG5n4t9*YWA=j?xk&ANX zGd#eKBf*+GR~B@?5z!T-TFPla;a7lebd(#8lUhZ}#aKu846tFY0kOGx*bx#+oq?i@ z6`cYXn``(S7=a*#cQvz1lUq}6$LMb?sJzG(PiII$w3p;Hh61+2(uk6^VDteODp}GUA(CzgZBw~^@`lwgANfC|(OnBf}a3jmp@g`MRs%XN;Mo z9ij_@nR7PAQr9-yzi$ArJWE0Jkl=>G(6!&|?_2bB_(>PG7$E-sW6M(k$bxZxi!T4M zANZ*VIQLnt-ctYef&JU3pEfHH0@Ky(^qZXYn_KoL!z>H%#>enKA^cn4e%bvTfR9zx zYF?D)pMPlri~z+XAc+2Q&WC0a0|SHp&e@e6VYB*(Ie1;(7XuRu8<&V)+84u5UplpI ze*5>2KljexY*|au|JyElG9n~j_2Yk?_3J=)Os7*;vcK&Ly4hlLXFL4=E)N`NDxKOG r?Ekl2=9o^D@9LO<=KrrBJ!kq(KN6NQZPI-67pw5<}0>Eg*_g(hObF9Rmz4NQZzR-QC^Id(dy( zpWf%b|2+Ra7tYL_*}d0Zd#%q}dz}eTl$X4Z@e~6A0pY&XD=}pRggY<<1jH*e6d;8f zTgU|XKsFPW6GlKN4aK}PybF9MF@B{ihk)Qtg@EwZ7XjfMNP4@9fZ)W8fUy1=0f8qL z0fE3au||mx$fyHpN}0;ZA zNlW!vJ9Yk!hE|5Q7CUtTK&d;ai2sfx73uHR?!Z#-{C$sj1(ZXuz$4cNKG1DnX*nPu zJZQOnAwuaMJVQXZi*KQ->8L3u%VTTYgM@^H&)&q8M_Ekbuj;@z zell}MM_V2yCKneMMi(|l8+$V*7H)2CCT3P9R#pa}1cQSc#L>`|0pdXZtC7Fk5d%3G z+gsQ=TG&8HZre39vT<_cCnLM<=%2q|<1{h;rypA4sGtU+K9#L(b;An1d<78%T>*PqnK*I7r4e>vF_&LPi2gCPkS$Tv_9R71K|9>?5$!4~O z_6{IbH(L-tnKH=1#>w6o^bcSDT9aSzfA^wl<78}pTV8;bkLjrBuw>~zD$_rtd}!Yrn+(1PIwt7 zWl9BN8;k2ax%6_`@jRJ2nf6Q}7`s|d^ni?#Jo_qyfc*PK@(RrtwUduU4hsSC_e&9( z#ANS10usq@7nUlJ@qXRG5cYcx0umPN^vRzsq0s^v!bjE2TEF*!fQY7bdG9R(+HV&! z9wORU>bY(#{qH$%5q#;rP=22W0`j{+1Yc`KPexh%KPHJFf-iXI&#?-NBZzS32oO+V z{xL-a(x8+-=z-vqN`^rCOf>~F^6np*Z^K&q{+PvEAw}%BVfXX;K6JkOBU7kj-t50w zkQ_9j4wRvlCKKU5GJ)k_`)w_NufTGUy|;u%a*ZV?6IohWkq5Pk+_`fn($#Dd<9AYI z5&P`!+6D#(XSTG6u_p9*-NoYUSRSE99_?9jdiL~bPI`JXcUegZb6`ja72?A?e@yc+ zFJkSKe0_cWA}zyxlsh0(Q)QD3?>E^$q&tpJPE_RN?vGh>9;qp-s0`U)q@|^`T|=31 zejkOh5aZ_3Kb zFQ}+YS*Y)l6x>Q-g@5l)f~})5_%HX?5%6$FkI^73-h zt6XvQ-i*09j_{{sRB=U4a1s870Nx7a-0IFSCha!`QTkqU454KKpFY`cc+Xz@tzFDq zX6nq$-Dy2sw&GoD*x{2>QaUYw*xLT49Q(ciZ9rSPd>DJm3itQ8BEVUyJs&Y#| z5S?B(ub-w@dcK9!96jYxcZ~RQ-Adg5WlH?;o61dA=;XfFiYxo?0+wX(Ry~grFueHo z8Ix&x)=B*aR9x6IF2jZiTO$aal$@KJEBE?=!Q+;e=yi2Ky?W+dheWyoC6fPbdcH`A z6do8Q#hN+}9|gP?G%Vzo@wK^kJRNs5h)9Am+qt{bnkn-bHcZ7^*5PZHYI$ylyXf_p z9KXXMUkXH5lRKMb0(%$UIZs-j8m-EDvhg35wXe18{Wv@!@Jc^VNYGYG;v)YFUY6<% z2sr>luDJeCsEXtOvUzR5F7Q>Bso{cxL6bc}-)oPdX1j}b6-qFZb>|{vAW$tRvFtvk z1E(7Kw|*8bu2nCidy*9#_u0cY+H9@vA^s;=h!sKV?^lY7kLLTJ!1prrT^Rq%NlCsV z8J8Ie;)|AwM=4m6PvUadFEe9O6zE3ivpj1r-=J325+wK*Iak?_4^f{@o@~k%+;zt+yOhTHrfs8 zS2zdcwaPr}yl9Eutr^>yz931WrOCdzSjj(}FyPDWZP>ZeQ41FQgI!e7efP#Y_L%bS zu|B1wq=86vL(cJWH_Io8k!r?RUUS8Hor&J?!5n+BPt?B zghNBgr1?Vcrao8{ z01Lk(#w$ktPZ5lO^jQ{wE>R7P)AxS|Rw7}5(SYWqT8Hz;&{+WkMDe=D_2nPzA`Gw# z>8aA*|4!)t;tG+lMj1TVK|zWXZ-i`QwLSOF=#8|Ate=s{ip!_js#8!@eRFQYk96{z zlzB`JEECJSrjj+e*+F6fkwU*@7ZG#*Q$mg{*Tm>)mrqS(yMpA@kN;DUzs5kDHJDXF zcoK>RLGpZlA3~ zUKYlY91qKt!TKXq1>xuT_!5tZa>d+Zb)JhCwYQMz^E$7E+FImWooTjlUmP8TczMd2 ze0oYi1kONXoKr%HaCT)MVOgeewcE$Ar|*RrcXYNIB>C? zA(Pz;5SD5`})}WE3?J8@2~1N zUu>IEvk|DBHF}l$${0sHzA9FwD)}p=7O!(*Pp26NgN`wcQ&gSQ)f9u}edo(M%-(gcs>lCM zT_g_JfiJxE>LxO<{H1!cPpb(^I!2eB8**B#wGW3Xv5(g!mNp-v(1T6P46Sx5z9SHL z6b}zyWO(QFthjEzt&j;GDOz)eQ%c1+KdkOkY^c=A^EkNDvRCMcY6O)&!;8ra&>xc5 zNYgl{;Ew$i&f_i-a`41#+C8=_`TWy?=F{b-;PTaNm#Oprzz(SZ_Gnc=Li^;?IN2v4 zgXew0T7h>Z31SGLn-n^Gd5J^jd~dUjm!c{56?PQYbaxwvK5iAgwbp7LT+Wv{NZ)BP zb0}oNC)s&@$I?`RhCqg{G9hZDyg1#fg>C3Dkr|kaWj--G1a09Se!Yc>6%YTsv|a$BQI-^u7xT5;BD|yn z{P{}=+aR?_Wxi5A?=vVuv=?@xXVQIgGwPeW%aht)WSd4PS%svn&j|zNlEh0b9A131 zvPN7ABJBM~#3Cd4bahGhx>*961SQm(A0q_SJ22yz9Bk92W@7COhgh7LP5H@_f;Rh;c;7q@`4<_GtfG;WbFB(Ip}GgSMl6x7 zgR^rNUyg)jE|<7hFi(qC?xu$-cGV=*eaUlm5dYrg)s{C%prUYv4vEPz(a>jF#9e-O zWYVGkZQhwC&ORob#pSMeOWRou^2)zT>U9(HCARiGQr@{fAI#gXNtn?O;i^gxsS{zU z#A(U#Wb39rlPVtH)>@C)t+c%9-VN)PM7M5Xur3uVjYabZixdB=Za(=T_-?wISAP5i2mC~`I!NlrI;YH%_}(I(v$n{6W6_L`i5fhDnr^jD%PA@q3pDTrCWGw#fW(9v?Lka6E|*i_>a1|uw0_!$x{A*IrR^0vWF%7A z*Nx*+(RuF&d&DD@wemny;PAc{W!N%j$lXGg(2?ooVvxf202Y<6#y?7aSE7+v1b<@3 zH9go1%a(FCVFv4J+ogzqu)z&eT;SB9Qh*RJ7;wT0+Uy)S^XJb)=5z6s=;q-%Eeyq9 zq4Zrw(Rp|BRu@2hI=DgFz_nDd#6wk~C>Z?oUs2R1i=Y+M{t4qwHUnbNht;|RzAs;n z=v-8+^*O_*}+kefHwvj}VvR$ev*t zhZTYWGl3iInq8v;DF>1G7Kv_N51O@ulzrFsg}t-#)bz;(Zbw$1QB667S`0%ytcN>6 zfdg$_9Li#kHY&a{I|370$iVk0G$@|YDAv0O7QNwZI*H>!dhe;eUNotox8$L^Rx~Q3 zK26jSDIOo;hMDT5O`}^pJZ==Kqmc0(^)Es7RTM$Q`P~fd=x|4%kmpWxD4<38U92gc zCSLH9i&s4BWB;)XB~Gh}H`Edzqd6HYdpLl%1MKa&&`$E*<62PpS+5oU^ls~>oTJhe z5zOyHWKxqVKM6R-xSk)2XpS)bWNb`_M<`@(oo9>4o@r!{l`0X1#z;~7{iEJw?P{IV z)JKvH<)BYfEFVM32i;=4n2hh;;SAj&fF+T6EiCv+>rQsSr_DHV<7CCivc%D3!jDaK z)bRShc#1uLKK^ubD4+TQ9nnc}sRE=>xIY1YTX8KK|iL>bK2ZkVBEeFhh zSBVIKfw-6IUnxv@d21~e2CQ;mY0;vp6$H+BuoJH(ME6y*P!(+n)~iK=nbg5@iu1Lr z(C^QRH&f&8jydKWQihO$)G!m+k(LVRtTpsR4|r%Nx5w}-QuA%xqEQA0rp~YIUN87B z352L`udr%QO$oZOF(2#D3`{G8)2F5rR+N*YmJ3vRarcppS19N;U!7ODZxJ@11Uwxq zisZ>^h*^wlL!=9+iiYQ1xT`)lTX7gL8|jS$>qVZx7V0AzEswNYAVz8G5fT14IOWHm zo`^b0d|b)CyxI{HJoUM#&P;r~7V7P|gSkhpqY_6>p+!)|%m2mBwOnZnY=s}nfT^QZ z3uxD6J9?gfq);zEqBoirg$K{c%n%sGp#A_=RZCwcrEWpue5yFvnU@;1@8$|Lbdp5; zCcNnEyN9aB+&nis^#M}ei~P^;FGBkP6H`_2I+hUWY_sZO(9i3Yi{JrA{E${5WE>xq zRm*rr#DCN2$m$?wO_K28Lt#FGv(*8U%w))OBe%B~9$I`S58Lt%AjX6vuTmrKjH3uC`6n&QTw8MS~%`bQq=H_aHnTd z)Zm0LI{ZQ4!co$4@&i^)o)_@+){AUt#Si5+tN9)|c`E6-7ns=9WbOv8hKqU+h^gF9 z^wQ)KB$6@vlVhWhA|mRRQC6Ng%W%P(jp-^VB48)lIR}Fid8>EQBlicNPE?Jp?72@I zgpq2YzW-bNh(sX_cc6Ut^SmZ5{@z1;8L?lm&9zSmiW|upTO%3^31RG#(=0wej;RwW zTmI(Q>-A&bDtGle`uR}$OnL&OzHKbDZ4t|SsbhkvLELd)=*fqpBc;jt>cehKe~hs; zlmuNQ@Hg$2LNr^M8N~^ml*3BRfN?hsifVP~T73rt)R?l0M>@{}6r&@an0~%DAWb_$ z^B!ONY039{j8f6~(k1(&MVzdT70QktbRQ}`ETDvjmI%R1QN)? zj+jkT?qu&t$y18m)JETECakBXekgY?S1^9zF> z2H0EoM45uKqTndfJgeNk@UjGN3z$dcil4Y-BtLm1iss_I%eYUq*{<+AAKK}l;tnpmglK#&`g6=Hdnep0qnOjkvB;dK?XK`>h zE%wdCu2aeRUaG6hYoIa&87B=@s!OcEj6~M$jSk^_<=E3KUmMII{XJxv1~fw&xi|7j zD-K2K*xc`F%~9%$TxzbTntbp$D0e>7cXxB;bE5`pEj2BBb7F-%WlmB(&^C_rr=74j;dty-of2kMz9fgWn* zpY)Rym%-JyR5FjY{06I5qqL;I$7@aoHU`Tl;J$PG#~*+d54ZvD%+?BAeotAexcMTp zhi+PPxgKQR*DDGs<7wH@CtygDk?M?>d?gRB*ozm4Ss2FOBCag*guL9d!y@j}(z+!a z5&EmmX0dm!eY^W(ZA3MbD~>jLf`1)Jk`$u_Luba%`p?iTO{?^=Lms1p)-W zxe0ob9hl-}3fE|QjfdVAL!9TQF9_sD%3nC?BOOl z3pDFsTD45iok&Yv3J>!IjF%B(rP+m>;QyjdWRWzt|t=7qf*Z2@!Y$&YV8PUrsT3d%H8m_yUlpPOP@!rVhk1!m$F(o1cpqda#j^0^? zn6zuF$b|~GOss8SJF3-WHujiEK8z#2XKBWcVKz~zn`_^*zVO|$PF`;6HgbQiifHSI z9K~D|qow#_;3#wf3w~W>>X$RrQn+bnbrN=WLbvK`xLDxGSnsrKxaoBv(!V=b7r8{j zS-0w)+3vm?CJk%EP3yW5KR@Sq{Z;DvScx0u62WrLG2TvtKatz2#rbsoLuGAu*31m5 z{?LYtXOu>4Wk1Z9;#(PAgmD2F{T)4H*ZwrLr1B^i_lYW~%U1p+%SVn?hImnR_*>cq zGABFC;vZltp03Quk;_16xy91i5BrwVt~~2B=w-_L%(@WACro#zzH5od@N{=*I!EM` zN3q4&>FLlB*uG((ailK(0iCy;FT^vX0~DXUR5-FK>1(oAxH0;l1Fw0ytaQ#9%|w@* zoNTP1IJNVwg&s@UmYVc_#0h3#$3Pv?WsxiUfBkPNuMmjFXngsTC6ffyr0AA)ix~UM z@VkYb)cBJs=d~-6olmd4E>5o;7kV@v?zFL>y%lL`w^xd!vJ{_`=I)JG-CpTob@SX-KAtR^g;9N#epV~}BE7~JT9WQhFfsxciD1w| zF7nr|k1psUHLqt-qqm$gts%+$VEO54bnc#QLWEPTwkdn_DeFtNKD-g&NDEd1;* zoSXIR)+I-3a_2C+BOwgB7X2tCYsd2-rhFmkYS3yUbL?P$Ere-Gc0~VX8o@K+dz^Y{ zW{7ni^%|=KWVP}*o<%woaGY(q;@8bqE|j01ZZyzO09XT{KxDt}L}b>=8S$g~Iw8AQyy7c?r8cVYq)L8R_l+Gt z^9u$6bx-Ao&mVHNnXmYrA1xrpT-=*#?mZzSJT|!@>RM(AH^kO$N^(-IBHZv~yvkG9 zcv;fucbCA2|GrqQ*7t4a!Pd>5Bh&c`C~R7ZpvzthyP~E9FPv_ESjQaJ*`k$;Z=R-3 zP(=!-RP;7W#}E0oVM$3?y>aLq$`xEqr}_eBu}s?=RhMkUHX8J|q2Yr?i9pKx^&uuH zudr>EEH>9@>jw11YXtK?b}ZS&>twW8a-&2et}fDd6+G_}+aLH4;^Irq)#2712APM< z8)FZwkM>iGjJs_TuB@tzR3)w~n15;iepT+kGN<&YTYTA6nKE8>PJ7}9uWY5j-sF0k zda>2mlZ{U0`|%}CcvW`$wV(Fw(~R3c2BPWZ5B*^9$KhUr`D^QqGHQuXXhn&3sP5-P z+9^q^+qJaCxT2a>M5S&T7R9XBcVFT>a$Iv zSM;R5=f0%9n@}q8c){F|O_ThR+;zCxyPGGGm<}Vh@|z3w}WxY^(3VTDn{} zkx|ubgbNfZu>2~#zQUg6GpgLg0BPi{`BrU)&z(4Vgk-~dr3we*u#&CUJ`*#orPD<8 zw4{-hm)w0?vTk(zpF{}$at0s^+#26rBEe$lGAn=_?$ki7_>fZ$t*WcJXE% zipedu^Ebw!MqD1|@p(^F@{_}|nxK7Ff--*e#{<8 ziEnw%3w&>Sa!9Vsfj{2fjzqhYm4>4}8SX?RTu z)cx4nhWvd|LuKJdT*<9@W)ntT7mZCykDXU9T{WYGKaf6ihz=)pK#Tz#K3dY)rFOz6 zy|K(c3D$TBMC0&&-MRmhf4@o1D%|H?U0(3EC6ZZ4uIpYy{XlHH(JX9>wYqE|?>JF? z?@)QwkcJq~j!Li~>aK@2U`!!bQOEc@#(2T-tK7GF=;)FDj zv^&reUy7hgXWpRktqSd02)AiWC69X)n^I{G6Yaoy9&Jn{ zquiZ#4gmabSL=P@%MghGDNIZsYg1a*j=rbUpA--{Hc<)~GwISRa)W?EC%9Ka9}m}E zSXc^P(;hhuMzVOIXF}?rBTM`E(trDPW07tpjW6JE0ygTmU?C}mJN47gBQ9r$m_w6s@q_e^V4bk)Ckz)Ov4!xs9U# z+o%ccYr_PaS4tJ-nD=vOH{A|B9Uq*G&&{Wp%T0JUl<8XXi~Lb^4Fb=u+<1ZQx7#(_sX-B2S@j87*UWuDpp- zB0Dx96Xza$$p?cpB9+XX(7O;=9;W7MFK5ONdi?4E@1nxE1OEDMdGRT$+L)2?<9nYH z)s*jfSZRGlRB) zx?`uAPvVJ{hCwc&7W628c|;q>0C!~)3IDJC2tZ5~k=d+;hV<;G&ei-fVw<^6f{RBL zrgfwK6F1B?@h?Qr8gb7TLeWk220s+ zO6qpr9a_Tb!d1%kTw^9UDsHj$e3Oh69D#MEcrI)q^AWVe_NNH}SP{&fw~7<(5kT8n z+b3vm8>6sF>~ZpHWp3pCYRZpN+x*SlUk^K#cd$Dvk$&zbv$X7irs2S0A?i3% zQsDS%_t6)Ge}%_2o&v7NkkA+hcHaWryH5d{|A=~Pk}6=BoBe~{lEW?^eKNA{xHj-^ z2~$G*JLjT)Qrl|&nW)ZpFOyiaNYd^3B|9PcMEm2?a1#aiyQ3UNOQvs>(*8u8dC(ap zf(kNgP+{TAAL4~*x->L2(-J7+-mL}=ua1fZS2;(Rd;OE0#xy#|RX^iMYdw>%N+R~c zO5lvSr-j)W_H`^Gvv+WOu0PQc^Y6H-bSyMY(%LN!d7l0;{;osMl-|znj3qkmJDJ4y zM1~(K+R0!c_=J(!rshY4IFH!oW|B$jnI5G*eQv`z*9W_bLYp|FcU*Dshv{EnhM0(W zMSi%2N*DY#U!`Rtralx}-&r(;`4#$Wp-Tz8(WC=|A)<+>5n|HABhpAT{4%fu6I=GY zlpB1GE%(54o@b*!Y7$Mob`e<}J{nJ%=z(9FNpI(Ji|`-SZAX_;$aplaPeeY8Oe_{F z1bo^aHUxA`MgF>v|I$5GIJa}LyvB1}7pvaTZJ+wGXRPtV=En7=s`y#=T`ZXKXI~az zIY?&Zh3cwms-ELiaiqK3E-}uR4vBBQiKMcpROJycvgdb&ZT0C8243<=f@EIo6Umb- zb>f$mKr1SA#&qaNJ1vs!1*A?(87(sf=2~T*G>_%r(|;E$+^)a>wF2JMKddUjUm@Aq zl$pK#IA%dNBqUz3E4DJ?lU97m&`EvpGWgxCnS09?k++WK243Lp>m2JXTHF;9Lf+on z=AnhZ%GDt49*AZJ<=4+NEA0Kjgpg*X*7i{A-M)awM}KHphdzE&3z47tYZp^rxgi znQF+Pn^h%-KXn)~1tPj6z7pMro%kYt*(`s*O5erG`2!`uVIAKHva>P`?>5p9f%XlH zjRv->Ka`x_YBlDi`>Ho;8dt}sRR_ljp`W-p-5>8-0+3;t2{t?k9~RIq)*jdNncG82ar^Fg7`(+T1L3?w?`L7ebj$a+gNa14fQVOG)kMkSeRk z78F?%0`T|Gl{}LG`r?Wj6pVqVLP{2m3DfB3ItjDBRTRdt$y}7=_{vsLh9&cB4RL?o zT08c+lYNW&NOI7T3&QFTB4<9w=eeJsniZwfO*c5{hTW`4He|cNw}AMZF(DVPvuHa# z-r=uk3`BG^pKfRClS@hBF^BIo^g<~=Xu^226}k(uiN?oUA%i%m=^*cjed9^pGN21c zZ?U4=u;$ldwEBLaHJI1bzHW4~j&0C4sWagu^T~ai zo>+KP1v?&A_I%}L=P_|Addkf3p4G#q4Bz5&7ci}jzXi0949V*co3!0X@YaT> zT(jcz&rcG701Vp+4eciofdE7{T8-^iN0A>4=^HslkE@KKZz)#@xt3VpW8l~)~tM)SlgJXP7)gxIG-?^)ZZho{F35zwR$pje(8ijy1@ji zV!9QXi5^HpaCN`NLVI_+AXh$1|>bW2DwI zb2Y8Yd`rWcDWBb}o$PquyKL=QJWs@uxdkO)lZWMKx;D`%#gviuc$L#_Uk-8li}b|g zt9sk6%U5jNQ@b%i!z67oKA=WnE%4d&p>m9aZ0=7517I*^6Ws z10Xe`6v+!;YcsQ#OD)AJ2$jTM(PfYOGVvUkvVTz1tqq(cDOjv^9xkTeWcapl(b{;$ zG&MJ0J$NQEyyL%za4SmDxR5S*=v2-3N%7)!#XWeta~&Fk))&XJ9dL5-?A&OL_wDr^ z=!kU3o|?1sHjOKSButg)0y5U?#Tpr9pW?vDAoWyXPp^fr%i2S&#D^!&aVp7_F);&{ zdww)Un88-|&Q=15+*+=RUrA;55^GOr^0xW}NldP!2u^+pGEzkgD=KU_YAx;BYcssc{Em@v9r7I;IvvD- zMyRiH{P&*Eqq{GPnLP7?Up0k`4@&DbwI|av$#?s|A*Bx8k9#uqqZRdm`SiqlR1U+5 zMu?FOK7N}w>VZL1^Hwi!0Hof@)uw}3CW2;fTDvdN#877wtCrF-pTvf#7+v>xx@)=; zt7@A0Gx4oU28cYuAf?+djKR~-(iic~Lz_6!OT7}OAsQ9F?k*=;;&Zxwz2=>h$~>}x zsW6O=Hr7cNaB+L2F0_ihYRa^Qhz+N=6dw)jU|26A1r**!H{)W9GMQzoL;j9;!t(`g zs2v+MJ$@))y#|#W!DF^$@!YQ?IL(aV^%VkG`^z z!`==u_Ru7j;WQarmjt9<_0*Oyn^`G-L_r76M|zN3-iV}!0IAM(R1B`?=b}%_GO&+z zAdQ4ozimo0s@Hqx^>c4nAPJM2EMdMmH=apt;QMN2qq}(h>cEm=7wCQ|ej z1AHol^*v9P>!(~yUpxZT`+cI}vG{aRA(@6|Oc%WL9?KyYecLi^}+&X$5tax(*ZWAGO>|V+XbB7{Xj3nH~7T zFg+~fA_Cpp#R6g<0qOT_Jz@3M@2paHH&hcivaE+6Sac=CiR-+46MMYPCtK-x?0%kN z;4PDMazYGDi0aeD&j}q;kxXlkJy?BVCL_?KgYG~=#q7(-qRU&Wvx9ivdR@$ z#Hy-8EpoNjy2Uzm*c8}RC}St;g$+&x%{`hzbNxSl+}Xx^bQ^|?%y6s3L`F?FGDi%i zfrnW<%Rw7FW@vWa-hx+GYXlZsZFs6&L88#CM!=7Rz)JKh7H3u>)uAzOc(SWT2fW2# z^RR_%WMW)r?7$hUi&z-UlrcmPVKc@VDxFjv9)lS3R|i!rlTJgkbK-K~6I*t@chIGx znHQn-5Di-fqUjW3+hq0oqR2`#*~J}%`Mr-w>Bb@yk_q=`lX;fBR1_ow0<<+@zEnXS zcyF{f!;!vm)yxWbAgx>*XjT!aWBI9_PRH?RouKsAb`pEHs7)(~M*iiRzMmuDlZ$4t|Fj2|nfck$ehi<$a^ ziEHD%F4`~Je;;}xCeqR{)%XuBc@N$yTLzC=_(NBMO4!O4jsqtHyJ>DcgupH$ckClO z2C=_JvJSeGR}6nCPTJ&9yA8WBf!`Ssb)e-+MM+(6#S$NW!jp1}I0r6DY=&j`CmuF2 znV*ckbqrE_chd~8%Okk8S_-4IFUo1j)X|C&|FC`rt=w7 zgA+7@gNE1?t`b5B`4CBY87&_JQB1dog6qgNt~#k#iK>=@LE6}GWr|1`6Pv?F6YO&3 zaM!5;_Aw`?@n`#5om2XO|f@JG5wmU27TmlueNurTjql|`zbf6)V_0K$Be*5-4 z?}5!@Av0G|mmY5-hi^zHURO!xn3>G)F>ay?S0y$L1&32vyD>mC&WN6txax~NH%t`H zs)Hs918|bkgO#A5{`nF0{X2l9rUI4>bF9qGz}2MhBT><>2d-itAQv19RJ0dvI(U2% zje2+xKap1SG;a-jaD9F!@e|EEg0Tl>yg!OFAGS8cguq7NvEBL)6a)qrcV{0wqw_1% zJP2G-?-F4Qs`5v58IXlhb=T}kJP!+o`k9zd;O;75m~5t1#ezVsyo zMV`^gS8P$+GK0{t)Z-J%g`}6uSG-8wR95O*%9#0G6Exi`KXf2~zR?7TAxT9?#1W^0Ut(ktYXnd0qz07h6A8g-X(B$)Dto}5zdHNNAI0$%^z$xIb`PkXa z2;*e&4WLwamvzX6u+!M_rhM3X^FS#XuiK+_KYkn@qFS+ijOGCeg*6tgKXucnmprbA za#WRZ+3gWBlDTFkw~M?a)h6aZcfzcVq-57p;L7x)8yWoML5iH;S=PH=6Q^2;gd zHG;`Go_k=x9?mmBKS4RD{a;H`D^cV=FpEtiLXi*`q5pC6KHrF)yaOWhbxQCu3s#4 z+2Q2xVX-lI>?=vSvKD_Wob1~Y1;VyC=jG(sMr^Yd?@AALRW0)?gg$1w!DLctYLbyS zOuqvF7k341`<*1|zbeEs*GD{ta_X@aWon2LO9`%>>=l!25*B?*&^7$}w~VvO*s)8kwrPKI<9 zQE!H!m|B8Q$s}vt@2%91Lf^#mhc%DP2flQn|J+qtx2CyeA0IxwK+qQJ;qwqGa8U~XMA|k z%`;g|@7kJ92ga_BH0mK_W7x21eNQ*n>E1+>D~=FqN@kCV$zexF+C}DgaKJG~rxhdG zcwVcMlW$EBG*Pd5&~##9+t|#+O^BfZMs1mElbF63W_N&UyF4?Tosqpg{K>j~P8i(_ z@{f=9R$4M+AWjA4ixpug4U0f!+@I$7mC;|oDC=$t_h-Abb@6KkdIP_uu7{XIUrPt* z_yOvt2uIwF2xK%~Hf#?J3j zaQ}A3{(QQ&6(8*4 z(H$Arn5VhI!*K&c7y$)P?WSuA>qzQ!LJTs{O7T#Siw0eg@XtlLIvkND>vq;`b{n}G zk=at}YWWrpDx3D~JE;jJS_z&T&|bHKAPxb{RQ<4vz?|ax+E`+4RKjW9Rx?vtQ)nt*cFt@?Tfmt+(wC;@&*b#&D$U8ix%rdmw>}L`L|B zLf;o=-27E!&SuXB2I(mi_AMSL;D_h?(Z|46p1Rv{JkZN_QJ(0J8BA|#YI*@a$flts zHEv1Yf>-3{*Jb!cPL0z(;|^ z#CNkaEmBooTYRVXJdNfv5xoffOHugkb6|(P2_8AsxxpBwO9SAP>JZ53sv0EG((378 z)$<+Orn@>!JxKTpe1%LvjRd~gP z`KT_*r0-?1H@8e8OBPuf-Th&N-PcuQj=Y|dLcG|4VMCFMop6!!=(#pIrN4g*K*XJu zcBOHT{qS2Cne`EwC$QJ;T8-&07S^rooD_;!T#vn=TOJhQsxcIi^Bk2^YN%S1L?@@m zN_K1UB|SP)%a^r#> z)c>3ycuEQ)w2n-4IJMbapR4v-2%B0htI|+t_!J$4mi9>aEVQL`NxuL>wThjLQ`c*z+7yPzz6HMM zG95>WZg(B8KaAVp8ki-I=$_L^_sM>a7zYOq%wNjnI}+Pr@#n)s+ue|Ky1@}?kwueGGQoUVk#M2>sZWT3#V&sipZyJXnrKKHb{sJCNtCU zgrch51!GfY?|%AmwZqHU_LQDRU~d%djEs*9Lzg%Xw~DVm!Ru=xA!mcAYhV3#(*KIk z{J72J=40CPaeY)AFzuDFdXLE?!cM~u z61q`Lyw&T)Ju@@*Dyi;ym^C`ZJUpR{YQjUZrssZK5W7;hz7BR(1kDowyUYqA0bn5f zI-%%UOuomNeSVqAVvy|35lK`mbRGS#)tcf$a-`J=47$Ot#= z2MHUGhgd!eqM)L-Gh>MUT45m)N3=&R8DUW}W>u0b6(=X(?e=E($0v;SDlEHGjUXwT z+@isrdV{0-A~tqUAb@m8CTDjO@T2Q9_Mw~Wy1U0>X_Zn~-CRsUyWB9W0RNDStlG-( zz8CJx8YG32r+1{?@BZ;xH$!YI6{jXBve{gTwX(V|J>aokn}}SO?ZUTCLl{wvXcQ`? ztX2qsOQ!Woix|_I{VLLq%)RY7m%fSzy(mb3Z(fTZo-I!(C{OXBIU04O(4pQ)dK|kB zsxUu5r`Atwo+}_WwN$G$x~=M@Fr_pgvv4dqRI+Q18)sTXrgF^_n9^$4S=R?P(arM; zqRB7iv+hz{5GECRJ>YBY459Me?j}}wmFzWdFgQ6XtphJgr|e$gWf7N<$a6}J(JTD` zgZ)UlGOxnb{9;LZ8(MZ$5qvnjQEPrNaj`akVr~D7{Zi2Jd{0cwqL3owU98hW(62{Y zeb!`1LS>^Q%T^;E9}BcGqxzv9n#?@m8Jl>OU6r`W-|U17J?4l2m4t@upX<_yp-wht zinsJ{<~)jjo+MpX^-5+~?8IE=@m0F&PXE3!{imIDx4o-!61%rn5b8sf5TAvVdacLVM!g`Wyr>@0_f78Z zFY;jT((-6&-nD{No$$o>?H1!uz+q_mjO#NGiR>K?FZa+bJ9~+1=D5Lj0@0WGFVr#m zjKlcL>nkpDE0+<*ERS=?r#(OND16D3zM>m9A)haM!Q%lwQD%7DFVb$ zFQB~j6g+xALcp#@Fq17_O#}3zo#6sfFa&! zS)o*82(=_Gd;_(&cbh_;qfHldj*KyBWxvLzaxt&7w<86iivMdw^gF01J>g zi?4QZAezvIKf=_3lEztCH3wjq0kAc%(0d*Ogh_&5S=Rt|m3>{`?k@#cW4(yC=LB~y zb*c@nE(2KR&Mq||&wo^vJ%4mhb+PC_e;)d8mr#)ykh)2a`o07Mbo#%f?uan`f_K?3s?8yU(dn(h-*}u z#??UA^*d3=k&|l5g-kPzB*c*$u`@9S0siz}Hh=1uk(%7Lz z)})Kw1HoceZg)p{LOS%CkKF%1_TDlo$}f5w76b)BLIhMoB&AD08kA0@yL;&FP!y!Q zL+Kh~=tdM6T55*wjzPNNxzS(Y|9Rg}?|MHx>sgC6YaHjk?{oIqaqYdYb4JdLc)M4U zVZ%`Hi2G=wt-SEa#%IWX4V}P8;rn}!^@oKU(G9H3HqRD26%;;-fL(2zY>IQsRzBOb z3>Gv1RC}pcv^p-lKZXPhsM|NQ!@R7<9J=Q@Ip5Eq-a#Hp5qg8BuF5=bHdE+TYF6Q{q&+mLLH`RA`?d_@Ndf(+!-$r zcL=hli>g%cDx>&;65KGkBfq+35w%&{`$9sX(0h8j#oZ#DE*u<3>y|4;&f%#6wB_X} zE5*8_v`SSzO*lkKSR6OlVj?R_UMFJ}?}edVg(RacF-eVfEb>b8>P3C)%tCcZ58Lf1@`xA~OuiN}27OCLz%K-Q|P<`H6 z#HwD+;=_8av+)op+kMLnxCC=~@`VYLjiRG`h&Uk7n4BCIW4`bp!hgz`4b77{-FdH@ zEAM&uk@kf8E4h)jzN53Vq84$P5^ZG#YPsL;-IUI(RkktvZES^akXO4xhm5+P?>tLtyn}XBo7KWAUw9zNS6t^QPFKaq#A3@?Ho*DYZ zNfg*p1W#6yc6`A@c^7%DoVt2vXJ>1Tn$};YCa?!%pVlO#op)TFJ`pfqdG{77H{QJ< zcAf42`WuS!^{1_Ox4HJn-w}yTa_M{kzmpUdptzK+Glh{GivJ`fs1_Wvq0k%`bi>`! z&v<^wr!h7((WQBZQ#-3&s(mqZk_u$SwW~3)2l@0MVh2U?ao^0OxkRhe4$(DrkLh~oHMQ4tVHNySNWGy*mMr~Z^5*tLrvK(X`j!l zh=7@_vk#YRd%Gz#N`>$$>sn-G_F0DVHxwR6sgJ^%cj!w3mkX%!!L!pN)Rap#8x1LRTpk^b(JZjU5#3Vp<*@M}e7elcx;GyIV`FNT(UTpR7Hs?t0lua7yIuI@i@tlbt!mhFyUAl1~K>>AgU zk89U&0LW#HfLpRQm0338;~#cN&t49swvgr~PHY3`@>?Cy8xAc;jLiWQOAu72DE-xa zCG6l_*Kn`@{LIDl<`6c!b(+rAVJx!HuvKSA zBax0F14T}%afiHWnrMq;$eNlgmb$y4m{yd@k88gVY0Eii@Ncg-1+&J_*}TiQ#2V)zX)8& zSncEe)~b#i$|5xZWdBX0tLOhcGfZe-P?>osUON4;AJFsB&4iZP@{DU21{tXcbY(Ze z9RiEI-&a5%$Gl9C^_tqOwdy3Nhtv7Rcs+luB*id*x|!b?sG6${HCEuSIu*%+p@MaS zM-PnVYe#?@Y-VwFF|Mq-qc-^Lp5+8SbB1qJc2q;_kA?{#X!RJi3urm5uN z3+1)oMN8;w$*h-&4>17NoecH0?na-qMmsV8+tdGDLTOpVGt-j?Gg$=JefQ`lhbz3c zx=1PQj8|7@8F##LAOolAHjljmki6`5c;BQ~q|Ojq98cMUT~ z1w@c^hZDB{Rm=g#Bcz9$S60a*V#R2VTrFlrGR)l~ne3U!F*H}(n^P3XFdmg6`n0E8 z37N}okxfh2C25p<3Iy<~1|CSQjYS#XFGic9Wz$?Rmm^*JISN?;m{HJ;XhVg2W1#LY zP)J`KYB8(Fr(a9?4IL#m0$fm($vm^;<$lsuZT5o+mz{qJj1RG|ki7gNR5C53fs$I4@pR<}ZC5VlJ&D>3kGc}!| zHSPnGsZ!eGjg<>E-j_SEff}^rw%HQtG{H-ls_rj$9~}Az!8zjzQRMy93$XW|Ot@55 zRxwyx^&0D>TlA1@5y>l-m@Tm zJE9GKq^JfxEfGvcj-|m@iqydqmP?J=&{tgSJYNO3XJ9#QAfTy2Uft#}OhIK=k%>0Z z77R2drYBr|a_nF_>JSR)=MX7sAg{mBmL*m97kvy(X@<3d>LbZRywBTwJJdh#X#q!C zJoDHUc??eT$)4W}$n|;kv|-MkOU1{1U;T8_YDUFN6u;yR6LZ5+H4s*lSrGq()mzA4 zWoOYqCGqs1bmk!7{2l}E{{HNt<}*Z-gp_y zZog5m&zNxYAr^w3zNbT=|7&?)q-QJX2&(XtYWSZ&;Y`7kY#YXX{M|wG^+3Bu5F6}` zOAX<6-fdYn3@zLWa%+H<6hNCRDho6m=h1_KHp+$-l_F$h{p2=2LD<9UG;ukpk|~%d zu#q4pZ_CJULb+8eC`Xh~}EqJvfqoQ8RSD=|3uSijt&` zeW@zKNVqc!bFOc2c)8;937kC{!_y^E@Wu8uV`>@vZQ_W7E7;puDkhPDyP>MCW-1Cg z7DO{7CG`zPyt#xMkbLq&2D^uNUV|v1u`8lRp zKPFEuPo81|e0cV2KKws_rjnoY)bXmAWAPfh!hXz|EIwpOcJMGamm|rhhAGgG&-QR_ z+2)Idi-jg@210Rttka!_^!2zO%Z{e<%qN8!P);V)3EpNZ8J~UfWn2ubw zb>aAwr`$&FMA9ix(`1=XEB;Ay?1{wQtjUn&H_A=FiC%;;Kc#vJ5K3sGjTGwgVl|>9 z6MNId5E(-JO?Pmf#s2CbzmVM@$DNYA)Yf#Rg8C|5ay|i^RF!@JZ*Kcf9P%zI+ z*1Gp9UugzX676CbGgzpPHuX)9W9evU!i4v`Uu&?6<7!96e{1CoKrHHgSNis2VaAMJ z4C2cOcDh_~7_*Ti7*rwuJpn7h64}PiI`1k5)T%-i z`sC2JvOzj~7Y4~oeJmdHg5is;woU!(VXRL;&ghJ(7BFSL`r{#~EC%t!?b@KphJ?j0 z!V6ZCY_#a&u-8#ki7}z@yB?5}Jz$>&#^hKzf9Mm%nS8-Xk>2`r`!LywU3I$SpCzds zW+->B_mI+PL+_AF0X_S3x&*wxbAOGXa`u?VK zoRI>sU$ zE&?tBhlZ1S(dB|erDiR@bZWTner3XboUdgai=%H_R6laqG}2h}cmSO#v;9ZRmN#W& z3`oMBaT}m^502q8f-JKGYw*7i;j!Br5q4N?&wrPTEjl-4^S@8zia{Cgx*4hZnda6t zb^$hdN;SLvly|h9VO=-dUMy;(_(aNgkRF-pz3LZna9e5ezb7H<4Whj`z`01wbTanZ z>Mk1N8;i39cFuG-@4U?PziwLR1%CjG;#F%*1VbJ9_#}ba-?mpFO=4Gu9q_iK8sI*k z)@jdPVp##j9V2HD9vZG)DKb=n22R?*C0pI1g2BUto?EO}{4c zcRwQwAd~SEhRPus-8h6yPXSHsB_4z_=)qfheQ0cosd>$kJw%Tm#f^SNlg;iy{-;sa zzN8`%#*%tKU^Hs`rx2yF+weqw^}TtfxrN6Jo*hO2;THsW5-ABtHKQ*%dElKpV$-{? zpOV+Dq(xq=qN`oA4dG06L2|^r{B%Il`4(6IH_G@6`o5sCtx(6SAh(=ZTzs3hImpLo zKCE0`k@YsMJtTJ2ax<1&Sq+*7fVp^> zS=$H(@43OwxWMJ{PaWdBEY}{3W8|QM<;dUzD+PV5_YTyn!gGW?=o87~;q(M(vW2W4 za0WAK#17iuxz}|D){Gai*GXg))y}jm%IUrH8xEYK$9(=xc%t| zxMHV@Pk^w@?d}!z1e^#{`@Nc2{cU`sboiC+lU7SmKBzaYOvp1sU44|m;#20LYlg|Jm6u9euTt9qsrGG7F2IhbG^TkZWGrZztL@33 z6Lv3Ib|R2f_vW9Ra06!-s81h}vI(5uji;|L+5YJCVo;RR;1SQOl$I=vv7!{md*Ns_ zXTGVJ?QdJrUJ*0tVE@&q`#rRCNujFM$lcyb5yq4kYA9RHwdd9E_67iMPHgqDTT*JW z!go$n^Xc&;yVg9>1(`?LQ~HbP^Xl4!x_LnxV~3ZdJcHWcS20f4{14O93ejfeDwU|+ zbb_M>?vt`kx?uagb?ei~w8#6A7vR2$OOx04IMUN)W*iScvudLs8$TT*n(?7&JU%C? zupYYSJ{Q{YoX%qKV7I1;@Ktcf2aNaslDwhBd?h^mTYj_EHvFV+*L zU*g}aOr-;@ZBwhi_oyjM3e&831JJd=7X(MNn7jIq-aasGP8u@Hj2eDq!67!7%}y}r z6F;>iKp>7J+w$~FTQvEvCh_p#i~Zy%fNXiXK;fgxV%nQ3N!VuTTq|S<`?x#cV}) z#b+P<42W2oY5com{dajP2Lnm+(ZLsPjYyZvtF0;Y6xk)BmK{?}!Xy2vo~>MX^sxIVO&-2qYKuyW)r zzQs(V)!mf>jNFhMi{{<1emAaqXcS zIV64jd>U(=_7)5O%ncVwynw)m*RGD3$84RRlHF;0L?96uiCqCfZck_>j?gV6lwU7w zaV>|L=*kg3FNDFg{J*=1PGNIpmVoK(P zhO!V=MB!(dw}%V;c#50hiALso3;EZ}7i!zh3MeLC&!yRukU?0)g|cu{{_%(U%5fh? zbR-{S6v(9#a)KBc(X9u;|6GJdW{c}LfUyWcP^uD& z^_piQaE4DosN5xPSVxC!==~!W>6NEiOtcI4vVw&;nGjhIdv*9{GnKN`gikEG?kY5j zE&0MF5${kSo%n`szlHQmQpk8eR3d!FQ*egmxJvHIpBBTRlV3*KGsY8CBAC0kc++X; z7;)nKibx>_RQI|ePD7Nc#j}oTqGUwVHkgN4f5{sMX!xQmCwW^`o|dV+*71Q>Yrzu{ zo<+r8GJ2YsdE1^j{^>$2$YU~c?8e=3%1z1K3J{avC}>aQ;8B|n+eY}#?oN%LN=v~6 zXdL3_c{nnmkm-yM+N*cojJ0V>S$2F2#s23}z#;oDuj&JJOws z>EWyT9DlIE+1>KYYpb(?{BH|DBg0`sJ}D2H)MTBL$2cfQH`!8&sNBgjG!=(M^nN4`V(Hr3F~(GG2eB{`UTfJ; z{4u3k2)<>R7qp7h9l-`m$vT%LoGR~Ujz{Sbp!aDpQ#5g%tDm;j76dO?#9_r@RUb{| zl1)*|63N*+iKxv|YxPq2c8v%@)8gBq(Vq87QHp>uQ)Pk@M2U67 zY3QV-P=a-D*yWBK=u(5mv@fL+p(c2^f05Duq`I*_|KW2ZCeg$!7flY?v=`m(BL&~X z_t(fP*Og+q?i|I^jnGM**SaQhx>a$c`~2%m(6R{qHZX!4-Y72iW08Bf2YL=lP{KdZ zZ16c7)~__Og}a3q5nl^#r88REx@k!)k;|j(T)-xOxT!dF81&|K#c82731|75_+i!Z z!P4hik9ym$_R?qWc!9=?j*5-z_CI^*u-_SW$VqHIQ|xceY8AbwRsvE5m+OGwV3Wr2 zD~sfVD~-$WBK26?6KBp`%+yDPJGAGFT|eIO-%f7n)hb`829R62D5P8wr80HR-UeL!O=XCDq=O8Zx|25 zvSe(492;J;na}Lvayy)c?5|F}MIeSJE8nBuTaMk*bV;P^Ko2z@W5*(uc+~H6*;mBR z9|51t;dzPvuMhNlgSYDSKJZdo;>qyobao?4`j-%LoC+ZoBnk=&Fv4kZIixl5^y1uk z-qCHf$y+5Bbl>1^<5h!r4nDoEv6D@O{n*T4_E~DNTX%d>)kpfo6UzU%DZrnnpb}#n z$tTwbrjGLwM2vAM;uZAWsj$7HwhF$aSPYF1&5A%uFgeI;pN0+UW73cU6~@W5HQY3wIy%HXFiWYz93W_J~Pt zbA=UB^*XcIe#ns{h~jC;`Zjwi`hTpA%(o@`LExuPW%G|{7FavBp*nOa`%P0t>X}(u zAJ{%2#LDJB6uG&p9!{*euN>Y6ym%Pk#i11zT1p7Sq}J&<^N!HVfsjPMf4vLvE{h!r zxYhdoY5Z>3DT`S(>kxH++yF&0?w0srA?>$p$)P%dZdAip?llxdAs5v0H$*$DfRPo< z3mIpWsU(uYZugG5Q!E}%9BtEdE;;Q=yM?SSD60S9zaj#{J`X-L2DVh}rl<^x1{N&x z;tVV2V0n!6&#w_m>!>p+`PnLa$bgT52b_#I} zTD0Xk%%#i5{gJX#31UGKH^c|bmQuWBM@Z!?(rYL2t7P1lJueptf}vHs|7hbcz4Ti| z%W@krV4SpKw2p({;g3zRhcMh6;4S1h--T+C1E!7c+vz{&dShr`?Jo8*a#$!wf(n&> zCibJ;kQ5NZ3nH^(Lk4Ilk$Hophzi~5cL+LziWf!O|8K|JaL&17K~wHnMPK-by!_9NA0dYY3Gp9v{ewg}({xCue;7_N_vWOroAm&^hiuQ1}#{%KE=>gP8Cv`RHXLLZEt{W)s@E!59+rf`=}j(tfCxiE^lMk)oWW=!{!hY7qM zxE%JNsuZ@>?HnFeRCKreH){P7JQBvs>CtFwx0&hN3M;3)#&E2w{uUjTx|;J#xJwk5 z4Z;^ADGAklla|B1DEP*xv$V*xUmRxD=PG|Rz1aCMT%b8(YO^t;Fq9OpB3vYol<F5*y+Zy=wvXmoo7>Ohn zCe|ggyHu&_^@{$uy|1qzQpfEv26eV>nx}F&tf9P1J+a^J8C_TuXL3*JqK(Go87rhq{3aJF0WPMLRC z>&-|B$SUd~AluEax65`niUf=9iyQZn^FQ5UfUA~JjzNrNAuJ4kuCb89wVl*EBk;Me zUT$W}Pth)jM3a2cT2I+ZngU(Sm|#4Thww3nn%0$PnX#f9#b z%2l=Hgxqh%cK?)53)QT|$h@&naoj?Od<|RULh*#rNRW^5e9coTHn!-=X^7t~=T_oXB+l$xrzS#%{hH`V=2TQf;lUDy*0S16F)EKQ31c!yi<>fu6 zRm_oDXEU(6Y2bcF=-u!2GShup~p96)%vz2 zvY_n?Ry1iLfe-iL-Nd5}gOl|=+6=!<*lYCrro9O~s^it0jEOzv9k-{wH45MjDlb+q z^q?!h5=rqZO5>6ShGA^E6G)no4(?>m<|Pg#)iYSimEfjoN)%)-|~Zkp2^^(>SfGWYR8*IBvG z%(Ft*liih_=O%vdPKtH_fZ;BKZS>vmW8v*F%~K5oHp z)8oBBG;_NQR-S(|QxeV(dj?10&q7}tAYL0xpZFb*TSeF%e+sp_UhcV=@S9MxDnOJ5 zDC6`v|GxJxtHR;AHYH6?E5Zv3RPoOSONB-UT~2`c|HikW47!lC_^1_=-Le0|D(ig2m_LX0-3_bt zPxf8#v#j0C&epj-b1DDrK0;wj9*EqF6KmJdS}+1oedur1&j3_^d2q!CTdTmJO#6mO zk@cdG%fRV(@~6h14gGwxcFv_fhIz6ox;9XZDsd0oB5xDE8641P zt`%_0vD$Q7Zf>!)>0eFx_0B)FT;_rk(HyfWXNBK7Qt|_nhG7 zV0?X$kkzl4WJLdX_**38F&uuDhZui&?B-j3MLvW%|EXVG&mI_zpJgEFqo5BrmV<0o z*H;MEjG_1kYI>#QE5s>+joJ3<=2L$%3z2Qie8|7yhJf9FbV6iA8FxP#{2}RMLb8a1 z?N+6m0`SAZCyTg0aQk)HP1Qm<$H0M}y6Ds06o63RJsjxARlhH4e?0XP70u|Po(2D= z0LVmukN>aO|4$RUU(YiQrXVL=znYrp-Amp~6+D^5<8prQ+>Fm)+7CQdOU1+P zVpE2!2>O`93{4(TWOv6%8K9d`-0;0$C?Z%}4}R>(IZ4u~G<9_uE>{m&?tTNT}j-0x=#OpyATC! zoqKhQ($zP^6FigeXmUoZOII6a_~d4`D^!DSE6$lpG9a@&w`G$X9+4LA6xZDy69Hs& zP-Ru!XBVeOaW3KwDSd|@=WjaLPQpNzsbYVxX8M7Ffd}~bRK{H~l1(_En`(uYrS_lD zLo)X8DVB=JO%Gf15C{YDuTrgU-XFNqw>?ckrXy&MuGDLS$!%`UMXO#FEN1MspMR#c zz+1H)SA91m<^x7tcV~2N(VZ&yMAJAT&f+bNk4Pv=dWZeZ-N*e+F8Yq^;~GhmI{h4% zE3L7BApys|a%@mJEKB6;>}j~==*rV-BJAO8JT2TSUn8H151wiT1@oHs^$xIkb!l;z zUPU})Ru=*4L$46qcJU%23Dmoi`c$Q-B~H6A(9_N*drAD_ocV7HgZ4eZ)ejj}poW4p zVOMV};QnAuXDI|9cedS7-R`0TwiHmo;*Z1FFAv@8dC&MdgyGcdLr;>x%s9MiE4hmU z9_O;&bsFW#;RP#r_!P^tC7b%+&L;2kzDb-^+LO8{bLh zw02_b;9kjq{eVI1YG7eiF|4!l=YpHQJ zLB|4X0YSjZ0s0{wK60BhLF}`qjb=^`i>bZ!03=MEIFg7oKsD}xzc(JA92hM9REq~n zlJU6g6gCi`ZBW(cyNxkyqc+47ze@;R$o#RLU%j-MSbp6zN@VkhkvY~+h-u2mBIdJI zoAx}QfbXU_Fa=@qN5riR_L%P(^%Z@HqGbcN)MS;j~Mu13;1J=4(WmEQY3z9*qz*jCzn09a#bY!SB05ST#Vjh66cH^ zZJstbjr_O>+vLb5(j02{I!pzX9NaVXV^A(y>y9tBlQq4Cq}d{1v&jod=CX3oj+{Q$ z4QW(6uXyTRnG@fm$bPvwRt|IY(C*_VRQyO<+8>~oCVgnC8_v}4rUoyB_3;SG#u@_X zKT8+sB84{Q;Plut65d{-Rt2l1II8k!<$=lbv$Z6d6yRwtz;)~jH_9bVS4rF(6@65L z1HemBdt>cxCXvPoK8M^&E2mXEp*HK5sayybqucyXH$9_x2=QT;4U1>&FXY$}P!nGa zj7@{pdkJ(ZFb83cR=|VT-qQe_a&6sUEbJI4I$Xz@hy-7+w1@O>)ry~cB!bG4*Hx=O zh7$`=vuZk{t)v8h^b~t8N84?wds|((0?0^U zd`HGw80@~j@xZuU)5TAAmm&jY(4C48w|OZK?d$4}ASn(yd5;~31;iWr!-rF(-4W)y zeTvNE=8$Uh(4WkkC>5+}R2Wx~!;LU!5tOs_TekpywurD&wMNMyYk$1pTz%j48peu9 zE4PC_cmKA;_TF3g3ST+?x3dwxGdg~Rmr58ZH&cJZ55;1z!q;uAyY2#7FFTNV37rg^ z(v8ne_B?O~=R}al99@4r-_0icXK26d*`i;c7dUnGRF7BHio&C$fvBje zA9uU|nMYk?T2mcxE5YkqIa_@LaUV52k`qLJJ@bSrn4l9f6 z#i0VeCqU>_#os?&!;i30_js%`6ae6n)0c8FY$zz3^!*w!uLHtdV^(MV)IgJ7kHb+P z3Af0$2*?{W_ECWEIUzQ=L7nHzi1@}7x!Dwl+eSaA^wKHh04<-o9DN<2Ju!e}6(|#( zmYnqyZ?x7hyuxB~%H2QbG!)*IynWeU8GYy$H@p(jsHL!yY0~4iJb!<}uUX}s!lvQe zuhM7Ay)qtO8r~Hp>eGn}oI+u~@bS^f(e_o*oKXMWGh&Q=t>K<%4H(_6tHEqw)OCE8 z4^I`)7JHL5rDEtTtlXC%mBr-5{O($?i}HadB}ylBaK}415;C9XPxeI4s)u;VoPpRL z%8|gX>P)Mh<8RwH#@0YhjT7R~+2?Q_`Z?=8J1syd{oL4brH@#r`Xm>|Qx|6{uR24W z3c6g_#iYWB$F$E>^6j}nqem$c$32jhi4{48bgQJq#v+@v6jP>SM}eP1*v(yuL#9QI zt{E$Z%fuU&7hsK@5{|6{!hr5*?4#c zCJlu5w~x{o5~{z~MIEo+X722)QB-I!!p&F7WP5`~N&DuotGg%s<=(d+!y?;Oed!!I zcBz7d#W5?V4aVS!3enr2LP9D&a<4=!_WZy=SP3M{(6l4^o__s`DB0}W2! zFs(?fd9U{5n>)cyh)65e`m99msu{Vb&uZJDtvEEZx}~YQ29F+?CX(q11f0q$KSJRPA#* zuU}el9a58EW4Od|sCdHZoc`4tT}Q_X5m8aeL^k8rZ8V6uFy5+@u3#d|;oXg3uhvC< zm~&O4_rbbKqaqeUK2K`1X$RL(H$F^Tqh8+N5t8(0`^Q6q3vsDzwr09+$DjN!Usm=v z)N2YDpq3+Kxb02iYo)2BJ_H$qJ5wAGJeav{wOOI%FIfG02Sp1#TejC=Vgiq1ZSS_3 zQsWJ#w&W}3W}Ahr7mGh>aG8Y+wi)*>tHTxv&#>Z-W=OXh1C{cWRJ=70LyfxQMv{I9 zS8U^tiMCGpnnt6<(E#oC6MJ3C;O2R)fxe_y~SRtUJZ_lg7>5>{cRR_yRotuo+a4c zVgS4$_hcO{S3>tZM!dW(r9R;TQvj`+vRyjWrVu4Am!H*EQ|1~DeAL_yD-rJZL0!=P zyHZOVUG>-gB}1-m7A1=F(Ku0~T^Jve-ZSBEeKByIS{7$Habs4m0G(83DCJeXv|G#Kg_(>$^8_WA5d^iume3Or^^0ie)JANoOr$QNb$*y z44kY82r=Su-39tiL8a_BvTC7YAP4_=#cG0mqaxr}`T$7T3%_;t-2}9AKs|uyvLk{2 zCW>v90pusN zX7p{{7V>}Mz=17kFCCUc&Kpe{0i>GCpjE0>Wi=JM4)Fr0UrIpX`GCv*I6l9RS?zHO zpPLjG36JGP>PuY0qF${*&!O(Jnk)%R;%niy;QIB1Exw}uy|`K;vp{@tyU3o~Js?!1 zI{bkfP9+^fM|aKE@k-lb0%H130wEOo<$Zwhx4K?f#@#~_OruyO=JK6pFi?_|qfftw zUlKzuib0*Y0@c@r2KQrD#d-XU4BPGd=`C4|XMu4|0J>(Y1SuD}>em*u;**LtJnU%A zy!AVyGkSys0HlQ1y_0k|Y8fE0FKdH(cdx!YW!M#4{I+hSe2#mfU5K-wP^96MCF zJWux@D~7t|iin9xeTovLbXX4dxiIIL-J)*kOW`hEdY|tDq$^`U8;e-b`PDwc3FxkR za^{}79B!(E++js;AK>EZCvHu6ya`I;%{|%EJ(oL*r06hOIh=}uPvSCLq;Rb53}l(8 zL$oHlHCQ5jlB#uzps6&uWz0!X1sSnlDIl2}@?@lJ8lm*Bl$ladZ*6|0m?O=jvU+j= zp_1<^%bl-r@uYV9t23KbpV^67&qlHFrk|wl1tpiVIlbz`0(uU*s;I#en<%kL-p-QF zn%i|Q{oZa_XTkRBO&azWvDMv`#@4Zr4;!83Uei&T;^7m?_Kn47rkRry#o;kMii!ns z(rQhPe#wjZ;&%e4)nT9O`VL&W{QGljU~b?U4_L(^*u6f7oAvY8HO&ig=t#D8WTEsl z`A8R6yn-7@a-}LfSjEa&{u;3bnUq*xcwP0%)GoDS*&9cm+AnZ`vt_;}FZNA%B(+Q) zxh*QwU&X3~)+c+~RD^UB+K?R-R7Q^pULm?l6q7rOWY<759e3(Dc@NHXi!M~Koet%I zo>662fj0+zWmHi16G<>=J3?@oc~2+C?ze-8VvN|8F6x|qNdL=88Hpe@;+f{MKJ}r9 zEum+QrPDQ*hv+nJvo-BbzIlAQ{5;&)(P%A0d2qRklnrQ;Elyioc`et=k3!t)UvTe2@vcK6I=50Qj-0U!Y}1|6X2SOV||1iAE+pf?%7 zmRe!;(d=OwXg2glHVj9NX0=}*HaDMIL^@gE>i@hQf^MDbEvUJC33+3}^vINf{&;a5 zyukjzhv~A_k^Q(kWJb2dV)tx=cYwDl>BZ+^vcvs+cEUK0*M%zQ^dy>s>#VZkFQ79J z^Lo(f*V_T2S1-)n8ix|XY24(ad|;NPVKX+}O=XC#GlrJG2nEOwXTvOg-!OchS9(;V(kgaew&S@{Z*1r4W;$jh`2!6>4E8od=Yi4prEnAYsH@ZJs)y@>vSuPM6 z6_1a{>X$hEY-GKPP`a^BVEdlI)sX`8SgA+adAU^;K;s$EkS*jr@|p7jscGUNU~}fG zI3B9GCc9f9k!(91a|-o39H7LPLxZI%cBC&h-D+zGLW^ZxRtLM14kzic>vOW5XH((g zb?-(^z(ifUtLBiCmZ!UyG$j%yioL4uH-NP`s(*anU>hK&2oUqPSP0QeE46ZftZ+bG zF3oBiW*m#aR~%5w>cSVAJydggNU0jr*DaLW4*Sl1m=B|@8xhX3#c%6_DreRsnSFHN zjz>t^%Ttle-9)cXlM3e0?;EtGzNCHHbXI$2fcd*-6=4auEb8PC{9je-D2g}D`m!gB z!PSdnG}AE`%ZakG?sxaPSq0U6uHjD;wZ!^fGXlofcxIj}-`-3MS1mLO<+Rk&oIX3o zH+S)1rk@=qhtH~L$A{%SM+sI#(&Y)WdtE)d-O6X+!fugSK zqmR742d15wF7UhNK6u#SRluUQsrw=T7C>nl80`Go_!Fy($4ak`C8L9M^tg$RG<-Lo zBrrTYUS2TC%vcd196V;XE8SqSfZrYOY`?F$G*vC8?($KUK@+4r;5=i}Q4BWr*HpFw zXYhIcR0BoEnw-@0a7K$Zm$00beoJyTJ{jEoRwS=IQF9VU1YWV7A!Qll*DN$%<8yQ9 zG^J>&%F)$c1Gd1{M|ZFxD2z+$w7ipe#WFG=YSASx7Pd60i34K0FP(K`^#8U`pqOXL zrt+)oDN)=qm6{}7SQRSkH2r~XLTPTq+i2^~=L+0R)ie^iCzJrgd3xOx*4O+V?vOkF zGcvWJ0A$dKAQxLMD!L7aE4$#@nf$aY6c2Olx^oP0QHRdHb$_nn;#JsJVz zjkhfXOCwwbMdvta3l|kDj%eK~9nvqU|9R-B^BWVPT{lPG(8m4haZ1DrRavSpIXk#0 zUA?R&kih#bsih;E-e4+Nfb(hkUYOpey-$EAS2J^*?%0Dx79NZ8mJb?B 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 MI>vX)wVu5EFK Date: Thu, 18 Jul 2024 20:25:47 +0900 Subject: [PATCH 052/160] Create ja document directory --- docs/src/main/mdoc/ja/directory.conf | 5 +++++ docs/src/main/mdoc/ja/index.md | 6 ++++++ docs/src/main/mdoc/ja/reference/directory.conf | 4 ++++ docs/src/main/mdoc/ja/reference/index.md | 14 ++++++++++++++ docs/src/main/mdoc/ja/tutorial/directory.conf | 4 ++++ docs/src/main/mdoc/ja/tutorial/index.md | 14 ++++++++++++++ 6 files changed, 47 insertions(+) create mode 100644 docs/src/main/mdoc/ja/directory.conf create mode 100644 docs/src/main/mdoc/ja/index.md create mode 100644 docs/src/main/mdoc/ja/reference/directory.conf create mode 100644 docs/src/main/mdoc/ja/reference/index.md create mode 100644 docs/src/main/mdoc/ja/tutorial/directory.conf create mode 100644 docs/src/main/mdoc/ja/tutorial/index.md diff --git a/docs/src/main/mdoc/ja/directory.conf b/docs/src/main/mdoc/ja/directory.conf new file mode 100644 index 000000000..d91d78d7d --- /dev/null +++ b/docs/src/main/mdoc/ja/directory.conf @@ -0,0 +1,5 @@ +laika.navigationOrder = [ + index.md + tutorial + reference +] diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md new file mode 100644 index 000000000..c9bdbb14f --- /dev/null +++ b/docs/src/main/mdoc/ja/index.md @@ -0,0 +1,6 @@ +{% +laika.title = ldbc +laika.metadata.language = ja +%} + +# 日本語 diff --git a/docs/src/main/mdoc/ja/reference/directory.conf b/docs/src/main/mdoc/ja/reference/directory.conf new file mode 100644 index 000000000..721762fb6 --- /dev/null +++ b/docs/src/main/mdoc/ja/reference/directory.conf @@ -0,0 +1,4 @@ +laika.title = Reference +laika.navigationOrder = [ + index.md +] diff --git a/docs/src/main/mdoc/ja/reference/index.md b/docs/src/main/mdoc/ja/reference/index.md new file mode 100644 index 000000000..10c12bbee --- /dev/null +++ b/docs/src/main/mdoc/ja/reference/index.md @@ -0,0 +1,14 @@ +{% + laika.title = Intro + laika.metadata.language = ja +%} + +# Reference + +This section contains detailed discussions of ldbc's core abstractions and machinery. + +## Table of Contents + +@:navigationTree { + entries = [ { target = "/en/reference", depth = 2 } ] +} diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf new file mode 100644 index 000000000..e7bc66869 --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -0,0 +1,4 @@ +laika.title = Tutorial +laika.navigationOrder = [ + index.md +] diff --git a/docs/src/main/mdoc/ja/tutorial/index.md b/docs/src/main/mdoc/ja/tutorial/index.md new file mode 100644 index 000000000..f28bbafb9 --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/index.md @@ -0,0 +1,14 @@ +{% + laika.title = Intro + laika.metadata.language = ja +%} + +# Tutorial + +This section contains instructions and examples to get you started. It's written in tutorial style, intended to be read start to finish. + +## Table of Contents + +@:navigationTree { + entries = [ { target = "/en/tutorial", depth = 2 } ] +} From 2ebd4dfe6942d3c3089a1f5f9a4cdb3aa6911b4c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:25:55 +0900 Subject: [PATCH 053/160] Create en document directory --- docs/src/main/mdoc/en/directory.conf | 6 ++++++ docs/src/main/mdoc/en/index.md | 6 ++++++ docs/src/main/mdoc/en/reference/directory.conf | 4 ++++ docs/src/main/mdoc/en/reference/index.md | 14 ++++++++++++++ docs/src/main/mdoc/en/tutorial/directory.conf | 4 ++++ docs/src/main/mdoc/en/tutorial/index.md | 15 +++++++++++++++ 6 files changed, 49 insertions(+) create mode 100644 docs/src/main/mdoc/en/directory.conf create mode 100644 docs/src/main/mdoc/en/index.md create mode 100644 docs/src/main/mdoc/en/reference/directory.conf create mode 100644 docs/src/main/mdoc/en/reference/index.md create mode 100644 docs/src/main/mdoc/en/tutorial/directory.conf create mode 100644 docs/src/main/mdoc/en/tutorial/index.md diff --git a/docs/src/main/mdoc/en/directory.conf b/docs/src/main/mdoc/en/directory.conf new file mode 100644 index 000000000..2308d9753 --- /dev/null +++ b/docs/src/main/mdoc/en/directory.conf @@ -0,0 +1,6 @@ +laika.title = "Documentation" +laika.navigationOrder = [ + index.md + tutorial + reference +] diff --git a/docs/src/main/mdoc/en/index.md b/docs/src/main/mdoc/en/index.md new file mode 100644 index 000000000..a03b254e1 --- /dev/null +++ b/docs/src/main/mdoc/en/index.md @@ -0,0 +1,6 @@ +{% + laika.title = ldbc + laika.metadata.language = en +%} + +# 英語 \ No newline at end of file diff --git a/docs/src/main/mdoc/en/reference/directory.conf b/docs/src/main/mdoc/en/reference/directory.conf new file mode 100644 index 000000000..721762fb6 --- /dev/null +++ b/docs/src/main/mdoc/en/reference/directory.conf @@ -0,0 +1,4 @@ +laika.title = Reference +laika.navigationOrder = [ + index.md +] diff --git a/docs/src/main/mdoc/en/reference/index.md b/docs/src/main/mdoc/en/reference/index.md new file mode 100644 index 000000000..4c9fa2e68 --- /dev/null +++ b/docs/src/main/mdoc/en/reference/index.md @@ -0,0 +1,14 @@ +{% + laika.title = Intro + laika.metadata.language = en +%} + +# Reference + +This section contains detailed discussions of ldbc's core abstractions and machinery. + +## Table of Contents + +@:navigationTree { + entries = [ { target = "/en/reference", depth = 2 } ] +} diff --git a/docs/src/main/mdoc/en/tutorial/directory.conf b/docs/src/main/mdoc/en/tutorial/directory.conf new file mode 100644 index 000000000..e7bc66869 --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/directory.conf @@ -0,0 +1,4 @@ +laika.title = Tutorial +laika.navigationOrder = [ + index.md +] diff --git a/docs/src/main/mdoc/en/tutorial/index.md b/docs/src/main/mdoc/en/tutorial/index.md new file mode 100644 index 000000000..02a78d0ae --- /dev/null +++ b/docs/src/main/mdoc/en/tutorial/index.md @@ -0,0 +1,15 @@ +{% + laika.title = Intro + laika.metadata.language = en +%} + + +# Tutorial + +This section contains instructions and examples to get you started. It's written in tutorial style, intended to be read start to finish. + +## Table of Contents + +@:navigationTree { + entries = [ { target = "/en/tutorial", depth = 2 } ] +} From b7411ebc1d746c91a2b22901e30c131d857db943 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:28:02 +0900 Subject: [PATCH 054/160] Action sbt scalafmtSbt --- build.sbt | 10 +++++----- project/plugins.sbt | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build.sbt b/build.sbt index ccba39a5a..e973c2257 100644 --- a/build.sbt +++ b/build.sbt @@ -196,14 +196,14 @@ lazy val benchmark = (project in file("benchmark")) lazy val docs = (project in file("docs")) .settings( - description := "Documentation for ldbc", - mdocIn := (Compile / sourceDirectory).value / "mdoc", + description := "Documentation for ldbc", + mdocIn := (Compile / sourceDirectory).value / "mdoc", Laika / sourceDirectories := Seq((Compile / sourceDirectory).value / "mdoc"), - tlSiteIsTypelevelProject := Some(TypelevelProject.Affiliate), + tlSiteIsTypelevelProject := Some(TypelevelProject.Affiliate), mdocVariables ++= Map( - "ORGANIZATION" -> organization.value, + "ORGANIZATION" -> organization.value, "SCALA_VERSION" -> scalaVersion.value, - "MYSQL_VERSION" -> mysqlVersion, + "MYSQL_VERSION" -> mysqlVersion ) ) .settings(commonSettings) diff --git a/project/plugins.sbt b/project/plugins.sbt index a4a69bb14..f56e456a0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,9 +2,9 @@ * distributed with this source code. */ -addSbtPlugin("com.github.sbt" % "sbt-ghpages" % "0.8.0") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") +addSbtPlugin("com.github.sbt" % "sbt-ghpages" % "0.8.0") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 352941ef482622c6710c5f35cd0f62524875c06e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:40:34 +0900 Subject: [PATCH 055/160] Action sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 50 ++++++++++++++++++++++++++++++++++++++++ build.sbt | 1 + 2 files changed, 51 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1502e3254..a4991cbc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -333,6 +333,56 @@ jobs: - run: scala-steward validate-repo-config .scala-steward.conf + site: + name: Generate Site + strategy: + matrix: + os: [ubuntu-latest] + java: [corretto@11] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (corretto@11) + id: setup-java-corretto-11 + if: matrix.java == 'corretto@11' + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: 11 + cache: sbt + + - name: sbt update + if: matrix.java == 'corretto@11' && steps.setup-java-corretto-11.outputs.cache-hit == 'false' + run: sbt +update + + - name: Setup Java (corretto@17) + id: setup-java-corretto-17 + if: matrix.java == 'corretto@17' + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: 17 + cache: sbt + + - name: sbt update + if: matrix.java == 'corretto@17' && steps.setup-java-corretto-17.outputs.cache-hit == 'false' + run: sbt +update + + - name: Generate site + run: sbt docs/tlSite + + - name: Publish site + if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') + uses: peaceiris/actions-gh-pages@v4.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/target/docs/site + keep_files: true + sbtScripted: name: sbt scripted strategy: diff --git a/build.sbt b/build.sbt index e973c2257..d223bc9f2 100644 --- a/build.sbt +++ b/build.sbt @@ -23,6 +23,7 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq(sbtScripted.value) ThisBuild / githubWorkflowBuildPostamble += dockerStop ThisBuild / githubWorkflowTargetBranches := Seq("**") ThisBuild / githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))) +ThisBuild / tlSitePublishBranch := None ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org" sonatypeRepository := "https://s01.oss.sonatype.org/service/local" From 1a91a0fa49ddf0efc4e4659d17483bc6c71e6794 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 20:47:54 +0900 Subject: [PATCH 056/160] Action sbt scalafmtSbt --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d223bc9f2..09a9d8cd4 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,7 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq(sbtScripted.value) ThisBuild / githubWorkflowBuildPostamble += dockerStop ThisBuild / githubWorkflowTargetBranches := Seq("**") ThisBuild / githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))) -ThisBuild / tlSitePublishBranch := None +ThisBuild / tlSitePublishBranch := None ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org" sonatypeRepository := "https://s01.oss.sonatype.org/service/local" From 00ea6996c083f0ff11c1120fa27651d2e8229abe Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 21:43:44 +0900 Subject: [PATCH 057/160] Delete unused --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index 19c37762d..dbaf386b5 100644 --- a/build.sbt +++ b/build.sbt @@ -196,7 +196,6 @@ lazy val docs = (project in file("docs")) .settings( description := "Documentation for ldbc", mdocIn := (Compile / sourceDirectory).value / "mdoc", - Laika / sourceDirectories := Seq((Compile / sourceDirectory).value / "mdoc"), tlSiteIsTypelevelProject := Some(TypelevelProject.Affiliate), mdocVariables ++= Map( "ORGANIZATION" -> organization.value, From fdeca10d84772943226e75a4af147017b4e8e22f Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 21:45:01 +0900 Subject: [PATCH 058/160] Action sbt scalafmtSbt --- build.sbt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index dbaf386b5..2a3927636 100644 --- a/build.sbt +++ b/build.sbt @@ -194,9 +194,9 @@ lazy val benchmark = (project in file("benchmark")) lazy val docs = (project in file("docs")) .settings( - description := "Documentation for ldbc", - mdocIn := (Compile / sourceDirectory).value / "mdoc", - tlSiteIsTypelevelProject := Some(TypelevelProject.Affiliate), + description := "Documentation for ldbc", + mdocIn := (Compile / sourceDirectory).value / "mdoc", + tlSiteIsTypelevelProject := Some(TypelevelProject.Affiliate), mdocVariables ++= Map( "ORGANIZATION" -> organization.value, "SCALA_VERSION" -> scalaVersion.value, From 5ca03642e1afadcf6cfcf5689c915863bcb0b28e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 21:56:51 +0900 Subject: [PATCH 059/160] Added custom tlSiteHelium internalCSS --- build.sbt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2a3927636..f9c28ccf8 100644 --- a/build.sbt +++ b/build.sbt @@ -4,6 +4,8 @@ * please view the LICENSE file that was distributed with this source code. */ +import laika.ast.Path.Root + import ScalaVersions.* import JavaVersions.* import BuildSettings.* @@ -201,7 +203,12 @@ lazy val docs = (project in file("docs")) "ORGANIZATION" -> organization.value, "SCALA_VERSION" -> scalaVersion.value, "MYSQL_VERSION" -> mysqlVersion - ) + ), + laikaTheme := tlSiteHelium + .value + .site + .internalCSS(Root / "css" / "site.css") + .build ) .settings(commonSettings) .dependsOn( From 409cd15babb67ac54ded9aa887c55d2447d7e66a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 21:57:01 +0900 Subject: [PATCH 060/160] Create site.css --- docs/src/main/mdoc/css/site.css | 3 +++ docs/src/main/mdoc/index.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/mdoc/css/site.css diff --git a/docs/src/main/mdoc/css/site.css b/docs/src/main/mdoc/css/site.css new file mode 100644 index 000000000..5660bf483 --- /dev/null +++ b/docs/src/main/mdoc/css/site.css @@ -0,0 +1,3 @@ +.center-logo { + text-align: center; +} diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md index ff994ba77..a0c625961 100644 --- a/docs/src/main/mdoc/index.md +++ b/docs/src/main/mdoc/index.md @@ -10,7 +10,7 @@ laika.metadata { @:image(img/lepus_logo.png) { alt = "ldbc (Lepus Database Connectivity)" - style = "small-image" + style = "center-logo" } [![Continuous Integration](https://github.com/takapi327/ldbc/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/takapi327/ldbc/actions/workflows/ci.yml) From 96df0fdec53cb93039df08b8723b0a8485cc9e57 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 21:57:24 +0900 Subject: [PATCH 061/160] Action sbt scalafmtSbt --- build.sbt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index f9c28ccf8..f99f9059d 100644 --- a/build.sbt +++ b/build.sbt @@ -204,9 +204,7 @@ lazy val docs = (project in file("docs")) "SCALA_VERSION" -> scalaVersion.value, "MYSQL_VERSION" -> mysqlVersion ), - laikaTheme := tlSiteHelium - .value - .site + laikaTheme := tlSiteHelium.value.site .internalCSS(Root / "css" / "site.css") .build ) From 507b94ca2e2378d0118f72947503d3b31deb85e3 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 21:57:47 +0900 Subject: [PATCH 062/160] Delete unused --- project/plugins.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index f56e456a0..e9aaf6dd9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,6 @@ * distributed with this source code. */ -addSbtPlugin("com.github.sbt" % "sbt-ghpages" % "0.8.0") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.1") From 720089aba77ca113d759b24aa60130873bcc139c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Thu, 18 Jul 2024 22:10:11 +0900 Subject: [PATCH 063/160] Added css for table td --- docs/src/main/mdoc/css/site.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/main/mdoc/css/site.css b/docs/src/main/mdoc/css/site.css index 5660bf483..9697dfb38 100644 --- a/docs/src/main/mdoc/css/site.css +++ b/docs/src/main/mdoc/css/site.css @@ -1,3 +1,7 @@ .center-logo { text-align: center; } + +table td:not(:has(code)) { + text-align: center; +} From b300c4648fc93d230b8910a4a6560c5f196ce066 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:10:18 +0900 Subject: [PATCH 064/160] Replace 01-Migration-Notes.md 02-Connection.md document --- .../main/mdoc/{ja => }/01-Migration-Notes.md | 20 +++++-------------- .../mdoc/ja/{ => tutorial}/02-Connection.md | 15 +++++++------- 2 files changed, 13 insertions(+), 22 deletions(-) rename docs/src/main/mdoc/{ja => }/01-Migration-Notes.md (95%) rename docs/src/main/mdoc/ja/{ => tutorial}/02-Connection.md (94%) diff --git a/docs/src/main/mdoc/ja/01-Migration-Notes.md b/docs/src/main/mdoc/01-Migration-Notes.md similarity index 95% rename from docs/src/main/mdoc/ja/01-Migration-Notes.md rename to docs/src/main/mdoc/01-Migration-Notes.md index c225b9091..9ed0603a6 100644 --- a/docs/src/main/mdoc/ja/01-Migration-Notes.md +++ b/docs/src/main/mdoc/01-Migration-Notes.md @@ -46,45 +46,35 @@ Scala MySQL コネクタに、JDBC と ldbc の接続切り替えのサポート まず、共通の依存関係を設定する。 -@@@ vars ```scala 3 -libraryDependencies += "$org$" %% "ldbc-dsl" % "$version$" +libraryDependencies += "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@" ``` -@@@ クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ) -@@@ vars ```scala 3 -libraryDependencies += "$org$" %%% "ldbc-dsl" % "$version$" +libraryDependencies += "@ORGANIZATION@" %%% "ldbc-dsl" % "@VERSION@" ``` -@@@ 使用される依存パッケージは、データベース接続が Java API を使用するコネクタを介して行われるか、または ldbc によって提供されるコネクタを介して行われるかによって異なります。 **jdbcコネクタの使用** -@@@ vars ```scala 3 -libraryDependencies += "$org$" %% "jdbc-connector" % "$version$" +libraryDependencies += "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@" ``` -@@@ **ldbcコネクタの使用** -@@@ vars ```scala 3 -libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" +libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" ``` -@@@ クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ) -@@@ vars ```scala 3 -libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" +libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" ``` -@@@ ##### 使用方法 diff --git a/docs/src/main/mdoc/ja/02-Connection.md b/docs/src/main/mdoc/ja/tutorial/02-Connection.md similarity index 94% rename from docs/src/main/mdoc/ja/02-Connection.md rename to docs/src/main/mdoc/ja/tutorial/02-Connection.md index b569e5002..88409229b 100644 --- a/docs/src/main/mdoc/ja/02-Connection.md +++ b/docs/src/main/mdoc/ja/tutorial/02-Connection.md @@ -1,3 +1,8 @@ +{% + laika.title = コネクション + laika.metadata.language = ja +%} + # コネクション この章では、データベースに接続するためのコネクション構築方法について説明します。 @@ -12,14 +17,12 @@ ldbcはjdbcとldbc独自のコネクタのどちらかを使ってデータベ jdbcコネクタを使用する場合、MySQLのコネクタも追加する必要があります。 -@@@ vars ```scala libraryDependencies ++= Seq( - "$org$" %% "jdbc-connector" % "$version$", - "com.mysql" % "mysql-connector-j" % "$mysqlVersion$" + "$org$" %% "jdbc-connector" % "@VERSION@", + "com.mysql" % "mysql-connector-j" % "@MYSQL_VERSION@" ) ``` -@@@ 次に、`MysqlDataSource`を使用してデータソースを作成します。 @@ -51,11 +54,9 @@ val connection: Resource[IO, Connection[IO]] = まず、`build.sbt`に依存関係を追加します。 -@@@ vars ```scala -libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" +libraryDependencies += "$org$" %% "ldbc-connector" % "@VERSION@" ``` -@@@ 次に、Tracerを提供します。ldbcコネクタはTracerを使用してテレメトリデータの収集を行います。 これらは、アプリケーショントレースを記録するために使用されます。 From e71b2260b6e582daf49e4cd99bc6d3e03aade806 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:10:45 +0900 Subject: [PATCH 065/160] Create 01-Setup.md --- docs/src/main/mdoc/ja/tutorial/01-Setup.md | 181 +++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 docs/src/main/mdoc/ja/tutorial/01-Setup.md diff --git a/docs/src/main/mdoc/ja/tutorial/01-Setup.md b/docs/src/main/mdoc/ja/tutorial/01-Setup.md new file mode 100644 index 000000000..c3ba4f324 --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/01-Setup.md @@ -0,0 +1,181 @@ +{% + laika.title = セットアップ + laika.metadata.language = ja +%} + +# セットアップ + +素晴らしいldbcの世界へようこそ!このセクションでは、すべてのセットアップをお手伝いします。 + +## データベースセットアップ + +まず、データベースを起動します。以下のコードを使用して、データベースを起動します。 + +```yaml +version: '3' +services: + mysql: + image: mysql@MYSQL_VERSION@ + container_name: ldbc + environment: + MYSQL_USER: 'ldbc' + MYSQL_PASSWORD: 'password' + MYSQL_ROOT_PASSWORD: 'root' + ports: + - 13306:3306 + volumes: + - ./database:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 20s + retries: 10 +``` + +次に、データベースの初期化を行います。 + +以下コードのようにデータベースの作成を行います。 + +```sql +CREATE DATABASE IF NOT EXISTS sandbox_db; +``` + +次に、テーブルの作成を行います。 + +```sql +CREATE TABLE IF NOT EXISTS `user` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(50) NOT NULL, + `email` VARCHAR(100) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `product` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(100) NOT NULL, + `price` DECIMAL(10, 2) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) + +CREATE TABLE IF NOT EXISTS `order` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `product_id` INT NOT NULL, + `order_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `quantity` INT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES `user` (id), + FOREIGN KEY (product_id) REFERENCES `product` (id) +) +``` + +それぞれのテーブルにデータを挿入します。 + +```sql +INSERT INTO user (name, email) VALUES + ('Alice', 'alice@example.com'), + ('Bob', 'bob@example.com'), + ('Charlie', 'charlie@example.com'); + +INSERT INTO product (name, price) VALUES + ('Laptop', 999.99), + ('Mouse', 19.99), + ('Keyboard', 49.99), + ('Monitor', 199.99); + +INSERT INTO `order` (user_id, product_id, quantity) VALUES + (1, 1, 1), -- Alice ordered 1 Laptop + (1, 2, 2), -- Alice ordered 2 Mice + (2, 3, 1), -- Bob ordered 1 Keyboard + (3, 4, 1); -- Charlie ordered 1 Monitor +``` + +## Scalaセットアップ + +チュートリアルでは[Scala CLI](https://scala-cli.virtuslab.org/)を使用します。そのため、Scala CLIをインストールする必要があります。 + +```bash +brew install Virtuslab/scala-cli/scala-cli +``` + +**Scala CLIで実行** + +先ほどのデータベースセットアップは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このセットアップを行うことができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` + + +次に、ldbcを依存関係に持つ新しいプロジェクトを作成します。 + +```scala +//> using scala "@SCALA_VERSION@" +//> using dep "@ORGANIZATION@::ldbc-dsl:@VERSION@" +``` + +### 最初のプログラム + +ldbcを使う前に、いくつかのシンボルをインポートする必要がある。ここでは便宜上、パッケージのインポートを使用する。これにより、高レベルAPIで作業する際に最もよく使用されるシンボルを得ることができる。 + +```scala +import ldbc.dsl.io.* +``` + +Catsも連れてこよう。 + +```scala +import cats.syntax.all.* +import cats.effect.* +``` + +次に、トレーサーとログハンドラーを提供する。これらは、アプリケーションのログを記録するために使用される。トレーサーは、アプリケーションのトレースを記録するために使用される。ログハンドラーは、アプリケーションのログを記録するために使用される。 + +以下のコードは、トレーサーとログハンドラーを提供するがその実体は何もしない。 + +```scala + given Tracer[IO] = Tracer.noop[IO] + given LogHandler[IO] = LogHandler.noop[IO] +``` + +ldbc高レベルAPIで扱う最も一般的な型はExecutor[F, A]という形式で、{java | ldbc}.sql.Connectionが利用可能なコンテキストで行われる計算を指定し、最終的にA型の値を生成します。 + +では、定数を返すだけのExecutorプログラムから始めてみよう。 + +```scala +val program: Executor[IO, Int] = Executor.pure[IO, Int](1) +``` + +次に、データベースに接続するためのコネクタを作成する。コネクタは、データベースへの接続を管理するためのリソースである。コネクタは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 + +```scala + def connection = Connection[IO]( + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password"), + ssl = SSL.Trusted + ) +``` + +Executorは、データベースへの接続方法、接続の受け渡し方法、接続のクリーンアップ方法を知っているデータ型であり、この知識によってExecutorをIOへ変換し、実行可能なプログラムを得ることができる。具体的には、実行するとデータベースに接続し、単一のトランザクションを実行するIOが得られる。 + +```scala + connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() +``` + +万歳!定数を計算できた。これはデータベースに仕事を依頼することはないので、あまり面白いものではないが、最初の一歩が完了です。 + +**Scala CLIで実行** + +このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` From 41f32e18b5ae7526250384202b913fe64b2a5a97 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:32:37 +0900 Subject: [PATCH 066/160] temporary evacuation --- .../ja => old}/03-Connecting-to-a-Database.md | 26 +++++-------------- .../ja => old}/04-Parameterized-Queries.md | 0 .../main/mdoc/ja => old}/05-Selecting-Data.md | 0 .../main/mdoc/ja => old}/06-Updating-Data.md | 0 .../main/mdoc/ja => old}/07-Error-Handling.md | 0 docs/{src/main/mdoc/ja => old}/08-Logging.md | 0 .../mdoc/ja => old}/09-Custom-Data-Type.md | 0 .../main/mdoc/ja => old}/10-Query-Builder.md | 0 docs/{src/main/mdoc/ja => old}/11-Schema.md | 0 .../12-Generating-SchemaSPY-Documentation.md | 0 .../ja => old}/13-Schema-Code-Generation.md | 0 .../main/mdoc/ja => old}/14-Perdormance.md | 0 .../{src/main/mdoc/ja => old}/15-Connector.md | 0 13 files changed, 7 insertions(+), 19 deletions(-) rename docs/{src/main/mdoc/ja => old}/03-Connecting-to-a-Database.md (93%) rename docs/{src/main/mdoc/ja => old}/04-Parameterized-Queries.md (100%) rename docs/{src/main/mdoc/ja => old}/05-Selecting-Data.md (100%) rename docs/{src/main/mdoc/ja => old}/06-Updating-Data.md (100%) rename docs/{src/main/mdoc/ja => old}/07-Error-Handling.md (100%) rename docs/{src/main/mdoc/ja => old}/08-Logging.md (100%) rename docs/{src/main/mdoc/ja => old}/09-Custom-Data-Type.md (100%) rename docs/{src/main/mdoc/ja => old}/10-Query-Builder.md (100%) rename docs/{src/main/mdoc/ja => old}/11-Schema.md (100%) rename docs/{src/main/mdoc/ja => old}/12-Generating-SchemaSPY-Documentation.md (100%) rename docs/{src/main/mdoc/ja => old}/13-Schema-Code-Generation.md (100%) rename docs/{src/main/mdoc/ja => old}/14-Perdormance.md (100%) rename docs/{src/main/mdoc/ja => old}/15-Connector.md (100%) diff --git a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md b/docs/old/03-Connecting-to-a-Database.md similarity index 93% rename from docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md rename to docs/old/03-Connecting-to-a-Database.md index e5de2664d..dd64ef5a1 100644 --- a/docs/src/main/mdoc/ja/03-Connecting-to-a-Database.md +++ b/docs/old/03-Connecting-to-a-Database.md @@ -6,12 +6,11 @@ まず、データベースを起動します。以下のコードを使用して、データベースを起動します。 -@@@ vars ```yaml version: '3' services: mysql: - image: mysql:"$mysqlVersion$" + image: mysql@MYSQL_VERSION@ container_name: ldbc environment: MYSQL_USER: 'ldbc' @@ -26,13 +25,12 @@ services: timeout: 20s retries: 10 ``` -@@@ 次に、データベースの初期化を行います。 以下コードのようにデータベースの作成を行います。 -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupDatabase } +@:include(/docs/src/main/scala/00-Setup.scala) { #setupDatabase } 次に、テーブルの作成を行います。 @@ -70,11 +68,9 @@ services: このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 -@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:$version$ --dependency io.github.takapi327::ldbc-connector:$version$ +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ ``` -@@@ ## 最初のプログラム @@ -119,11 +115,9 @@ Executorは、データベースへの接続方法、接続の受け渡し方法 このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 -@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ ``` -@@@ ## 2つめのプログラム @@ -141,11 +135,9 @@ scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-P このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 -@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ ``` -@@@ ## 3つめのプログラム @@ -161,11 +153,9 @@ scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-P このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 -@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ ``` -@@@ ## 4つめのプログラム @@ -181,8 +171,6 @@ scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-P このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 -@@@ vars ```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ ``` -@@@ diff --git a/docs/src/main/mdoc/ja/04-Parameterized-Queries.md b/docs/old/04-Parameterized-Queries.md similarity index 100% rename from docs/src/main/mdoc/ja/04-Parameterized-Queries.md rename to docs/old/04-Parameterized-Queries.md diff --git a/docs/src/main/mdoc/ja/05-Selecting-Data.md b/docs/old/05-Selecting-Data.md similarity index 100% rename from docs/src/main/mdoc/ja/05-Selecting-Data.md rename to docs/old/05-Selecting-Data.md diff --git a/docs/src/main/mdoc/ja/06-Updating-Data.md b/docs/old/06-Updating-Data.md similarity index 100% rename from docs/src/main/mdoc/ja/06-Updating-Data.md rename to docs/old/06-Updating-Data.md diff --git a/docs/src/main/mdoc/ja/07-Error-Handling.md b/docs/old/07-Error-Handling.md similarity index 100% rename from docs/src/main/mdoc/ja/07-Error-Handling.md rename to docs/old/07-Error-Handling.md diff --git a/docs/src/main/mdoc/ja/08-Logging.md b/docs/old/08-Logging.md similarity index 100% rename from docs/src/main/mdoc/ja/08-Logging.md rename to docs/old/08-Logging.md diff --git a/docs/src/main/mdoc/ja/09-Custom-Data-Type.md b/docs/old/09-Custom-Data-Type.md similarity index 100% rename from docs/src/main/mdoc/ja/09-Custom-Data-Type.md rename to docs/old/09-Custom-Data-Type.md diff --git a/docs/src/main/mdoc/ja/10-Query-Builder.md b/docs/old/10-Query-Builder.md similarity index 100% rename from docs/src/main/mdoc/ja/10-Query-Builder.md rename to docs/old/10-Query-Builder.md diff --git a/docs/src/main/mdoc/ja/11-Schema.md b/docs/old/11-Schema.md similarity index 100% rename from docs/src/main/mdoc/ja/11-Schema.md rename to docs/old/11-Schema.md diff --git a/docs/src/main/mdoc/ja/12-Generating-SchemaSPY-Documentation.md b/docs/old/12-Generating-SchemaSPY-Documentation.md similarity index 100% rename from docs/src/main/mdoc/ja/12-Generating-SchemaSPY-Documentation.md rename to docs/old/12-Generating-SchemaSPY-Documentation.md diff --git a/docs/src/main/mdoc/ja/13-Schema-Code-Generation.md b/docs/old/13-Schema-Code-Generation.md similarity index 100% rename from docs/src/main/mdoc/ja/13-Schema-Code-Generation.md rename to docs/old/13-Schema-Code-Generation.md diff --git a/docs/src/main/mdoc/ja/14-Perdormance.md b/docs/old/14-Perdormance.md similarity index 100% rename from docs/src/main/mdoc/ja/14-Perdormance.md rename to docs/old/14-Perdormance.md diff --git a/docs/src/main/mdoc/ja/15-Connector.md b/docs/old/15-Connector.md similarity index 100% rename from docs/src/main/mdoc/ja/15-Connector.md rename to docs/old/15-Connector.md From 3f85b3c00144c7aed6467a927af01a0d34079203 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:33:03 +0900 Subject: [PATCH 067/160] Fixed Setup document --- docs/src/main/mdoc/ja/index.md | 6 +--- docs/src/main/mdoc/ja/tutorial/01-Setup.md | 37 ++++++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index 634601494..02302991d 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -44,9 +44,8 @@ ldbcは、型付けされた純粋な関数型プログラミングに興味が ## クイックスタート -現在のバージョンは **Scala $scalaVersion$** に対応した **$version$** です。 +現在のバージョンは **Scala @SCALA_VERSION@** に対応した **@VERSION@** です。 -@@@ vars ```scala libraryDependencies ++= Seq( @@ -62,9 +61,6 @@ libraryDependencies ++= Seq( "$org$" %% "ldbc-schema" % "$version$", // データベーススキーマの構築 ) ``` -@@@ - -sbtプラグインの使い方については、こちらの[documentation](/ldbc/ja/07-Schema-Code-Generation.html)を参照してください。 ## TODO diff --git a/docs/src/main/mdoc/ja/tutorial/01-Setup.md b/docs/src/main/mdoc/ja/tutorial/01-Setup.md index c3ba4f324..df65cabc9 100644 --- a/docs/src/main/mdoc/ja/tutorial/01-Setup.md +++ b/docs/src/main/mdoc/ja/tutorial/01-Setup.md @@ -15,7 +15,7 @@ version: '3' services: mysql: - image: mysql@MYSQL_VERSION@ + image: mysql:@MYSQL_VERSION@ container_name: ldbc environment: MYSQL_USER: 'ldbc' @@ -108,7 +108,6 @@ brew install Virtuslab/scala-cli/scala-cli scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ ``` - 次に、ldbcを依存関係に持つ新しいプロジェクトを作成します。 ```scala @@ -136,11 +135,11 @@ import cats.effect.* 以下のコードは、トレーサーとログハンドラーを提供するがその実体は何もしない。 ```scala - given Tracer[IO] = Tracer.noop[IO] - given LogHandler[IO] = LogHandler.noop[IO] +given Tracer[IO] = Tracer.noop[IO] +given LogHandler[IO] = LogHandler.noop[IO] ``` -ldbc高レベルAPIで扱う最も一般的な型はExecutor[F, A]という形式で、{java | ldbc}.sql.Connectionが利用可能なコンテキストで行われる計算を指定し、最終的にA型の値を生成します。 +ldbc高レベルAPIで扱う最も一般的な型は`Executor[F, A]`という形式で、`{java | ldbc}.sql.Connection`が利用可能なコンテキストで行われる計算を指定し、最終的にA型の値を生成します。 では、定数を返すだけのExecutorプログラムから始めてみよう。 @@ -150,28 +149,32 @@ val program: Executor[IO, Int] = Executor.pure[IO, Int](1) 次に、データベースに接続するためのコネクタを作成する。コネクタは、データベースへの接続を管理するためのリソースである。コネクタは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 +※ ここではldbcが独自に作成したコネクタを使用します。コネクタの選択と作成方法は後に説明します。 + ```scala - def connection = Connection[IO]( - host = "127.0.0.1", - port = 13306, - user = "ldbc", - password = Some("password"), - ssl = SSL.Trusted - ) +def connection = Connection[IO]( + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password"), + ssl = SSL.Trusted +) ``` Executorは、データベースへの接続方法、接続の受け渡し方法、接続のクリーンアップ方法を知っているデータ型であり、この知識によってExecutorをIOへ変換し、実行可能なプログラムを得ることができる。具体的には、実行するとデータベースに接続し、単一のトランザクションを実行するIOが得られる。 ```scala - connection - .use { conn => - program.readOnly(conn).map(println(_)) - } - .unsafeRunSync() +connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() ``` 万歳!定数を計算できた。これはデータベースに仕事を依頼することはないので、あまり面白いものではないが、最初の一歩が完了です。 +> この本のコードは、IO.unsafeRunSyncの呼び出し以外はすべて純粋なものであることを覚えておいてほしい。IO.unsafeRunSyncは、通常アプリケーションのエントリー・ポイントにのみ現れる「世界の終わり」の操作である。REPLでは、計算を強制的に "happen "させるためにこれを使用する。 + **Scala CLIで実行** このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 From e796c1588fb9d506323fa1ba30a7f65d732125cb Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:33:21 +0900 Subject: [PATCH 068/160] Fixed 02-Connection document --- .../main/mdoc/ja/tutorial/02-Connection.md | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/src/main/mdoc/ja/tutorial/02-Connection.md b/docs/src/main/mdoc/ja/tutorial/02-Connection.md index 88409229b..658ebd343 100644 --- a/docs/src/main/mdoc/ja/tutorial/02-Connection.md +++ b/docs/src/main/mdoc/ja/tutorial/02-Connection.md @@ -13,15 +13,13 @@ ldbcはjdbcとldbc独自のコネクタのどちらかを使ってデータベ ## Use jdbc connector -まず、`build.sbt`に依存関係を追加します。 +まず、依存関係を追加します。 jdbcコネクタを使用する場合、MySQLのコネクタも追加する必要があります。 ```scala -libraryDependencies ++= Seq( - "$org$" %% "jdbc-connector" % "@VERSION@", - "com.mysql" % "mysql-connector-j" % "@MYSQL_VERSION@" -) +//> dep "@ORGANIZATION@::jdbc-connector:@VERSION@" +//> dep "com.mysql":"mysql-connector-j":"@MYSQL_VERSION@" ``` 次に、`MysqlDataSource`を使用してデータソースを作成します。 @@ -52,10 +50,10 @@ val connection: Resource[IO, Connection[IO]] = ## Use ldbc connector -まず、`build.sbt`に依存関係を追加します。 +まず、依存関係を追加します。 ```scala -libraryDependencies += "$org$" %% "ldbc-connector" % "@VERSION@" +//> dep "@ORGANIZATION@::ldbc-connector:@VERSION@" ``` 次に、Tracerを提供します。ldbcコネクタはTracerを使用してテレメトリデータの収集を行います。 これらは、アプリケーショントレースを記録するために使用されます。 @@ -81,15 +79,15 @@ val connection: Resource[IO, Connection[IO]] = コネクションを設定するためのパラメータは以下の通りです。 -| プロパティ | 詳細 | 必須 | -|--------------------------|--------------------------------------------------------------|----| -| host | データベースホスト情報 | ✅ | -| port | データベースポート情報 | ✅ | -| user | データベースユーザー情報 | ✅ | -| password | データベースパスワード情報 (default: None) | ❌ | -| database | データベース名情報 (default: None) | ❌ | -| debug | デバッグ情報を表示するかどうか (default: false) | ✅ | -| ssl | SSLの設定 (default: SSL.None) | ✅ | -| socketOptions | TCP/ UDP ソケットのソケットオプションを指定する (default: defaultSocketOptions) | ✅ | -| readTimeout | タイムアウト時間を指定する (default: Duration.Inf) | ✅ | -| allowPublicKeyRetrieval | 公開鍵を取得するかどうか (default: false) | ✅ | +| プロパティ | 詳細 | 必須 | +|---------------------------|----------------------------------------------------------------|----| +| `host` | `データベースホスト情報` | ✅ | +| `port` | `データベースポート情報` | ✅ | +| `user` | `データベースユーザー情報` | ✅ | +| `password` | `データベースパスワード情報 (default: None)` | ❌ | +| `database` | `データベース名情報 (default: None)` | ❌ | +| `debug` | `デバッグ情報を表示するかどうか (default: false)` | ✅ | +| `ssl` | `SSLの設定 (default: SSL.None)` | ✅ | +| `socketOptions` | `TCP/ UDP ソケットのソケットオプションを指定する (default: defaultSocketOptions)` | ✅ | +| `readTimeout` | `タイムアウト時間を指定する (default: Duration.Inf)` | ✅ | +| `allowPublicKeyRetrieval` | `公開鍵を取得するかどうか (default: false)` | ✅ | From ba87a669b820c34a81f6a2d29423334cf6834646 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:33:34 +0900 Subject: [PATCH 069/160] Fixed directory.conf --- docs/src/main/mdoc/ja/tutorial/directory.conf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index e7bc66869..aa9215db7 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -1,4 +1,6 @@ laika.title = Tutorial laika.navigationOrder = [ - index.md + index.md, + 01-Setup.md, + 02-Connection.md, ] From 8d8739df0bbad70013992247259b4be12766632a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:33:46 +0900 Subject: [PATCH 070/160] Fixed typo --- docs/src/main/mdoc/ja/tutorial/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/mdoc/ja/tutorial/index.md b/docs/src/main/mdoc/ja/tutorial/index.md index f28bbafb9..64bc9da51 100644 --- a/docs/src/main/mdoc/ja/tutorial/index.md +++ b/docs/src/main/mdoc/ja/tutorial/index.md @@ -10,5 +10,5 @@ This section contains instructions and examples to get you started. It's written ## Table of Contents @:navigationTree { - entries = [ { target = "/en/tutorial", depth = 2 } ] + entries = [ { target = "/ja/tutorial", depth = 2 } ] } From 7cc21a01c44a1839836600df2b7418607dc15c12 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 19 Jul 2024 20:34:31 +0900 Subject: [PATCH 071/160] Remove document index --- .../main/mdoc/ja/tutorial/{02-Connection.md => Connection.md} | 0 docs/src/main/mdoc/ja/tutorial/{01-Setup.md => Setup.md} | 0 docs/src/main/mdoc/ja/tutorial/directory.conf | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/src/main/mdoc/ja/tutorial/{02-Connection.md => Connection.md} (100%) rename docs/src/main/mdoc/ja/tutorial/{01-Setup.md => Setup.md} (100%) diff --git a/docs/src/main/mdoc/ja/tutorial/02-Connection.md b/docs/src/main/mdoc/ja/tutorial/Connection.md similarity index 100% rename from docs/src/main/mdoc/ja/tutorial/02-Connection.md rename to docs/src/main/mdoc/ja/tutorial/Connection.md diff --git a/docs/src/main/mdoc/ja/tutorial/01-Setup.md b/docs/src/main/mdoc/ja/tutorial/Setup.md similarity index 100% rename from docs/src/main/mdoc/ja/tutorial/01-Setup.md rename to docs/src/main/mdoc/ja/tutorial/Setup.md diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index aa9215db7..e810d7618 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -1,6 +1,6 @@ laika.title = Tutorial laika.navigationOrder = [ index.md, - 01-Setup.md, - 02-Connection.md, + Setup.md, + Connection.md, ] From f06bdc9be603c97a0eef0ddc5621e03e260dfcf4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 15:37:21 +0900 Subject: [PATCH 072/160] Fixed scala program code --- .../main/mdoc/{01-Migration-Notes.md => Migration-Notes.md} | 5 +++++ docs/src/main/scala/01-Program.scala | 3 ++- docs/src/main/scala/02-Program.scala | 3 ++- docs/src/main/scala/03-Program.scala | 3 ++- docs/src/main/scala/04-Program.scala | 5 +++-- 5 files changed, 14 insertions(+), 5 deletions(-) rename docs/src/main/mdoc/{01-Migration-Notes.md => Migration-Notes.md} (99%) diff --git a/docs/src/main/mdoc/01-Migration-Notes.md b/docs/src/main/mdoc/Migration-Notes.md similarity index 99% rename from docs/src/main/mdoc/01-Migration-Notes.md rename to docs/src/main/mdoc/Migration-Notes.md index 9ed0603a6..309494e6a 100644 --- a/docs/src/main/mdoc/01-Migration-Notes.md +++ b/docs/src/main/mdoc/Migration-Notes.md @@ -1,3 +1,8 @@ +{% + laika.title = Migration Notes + laika.metadata.language = en +%} + # 移行ノート ## Upgrading to 0.3.x from 0.2.x diff --git a/docs/src/main/scala/01-Program.scala b/docs/src/main/scala/01-Program.scala index be16565e1..8e26ab76a 100644 --- a/docs/src/main/scala/01-Program.scala +++ b/docs/src/main/scala/01-Program.scala @@ -27,7 +27,8 @@ import ldbc.dsl.io.* host = "127.0.0.1", port = 13306, user = "ldbc", - password = Some("password") + password = Some("password"), + ssl = SSL.Trusted ) // #connection diff --git a/docs/src/main/scala/02-Program.scala b/docs/src/main/scala/02-Program.scala index ed95036da..69ab6c280 100644 --- a/docs/src/main/scala/02-Program.scala +++ b/docs/src/main/scala/02-Program.scala @@ -28,7 +28,8 @@ import ldbc.dsl.io.* host = "127.0.0.1", port = 13306, user = "ldbc", - password = Some("password") + password = Some("password"), + ssl = SSL.Trusted ) // #connection diff --git a/docs/src/main/scala/03-Program.scala b/docs/src/main/scala/03-Program.scala index 4e717c439..e3b6ba165 100644 --- a/docs/src/main/scala/03-Program.scala +++ b/docs/src/main/scala/03-Program.scala @@ -35,7 +35,8 @@ import ldbc.dsl.io.* host = "127.0.0.1", port = 13306, user = "ldbc", - password = Some("password") + password = Some("password"), + ssl = SSL.Trusted ) // #connection diff --git a/docs/src/main/scala/04-Program.scala b/docs/src/main/scala/04-Program.scala index 855e402ae..4fc0758df 100644 --- a/docs/src/main/scala/04-Program.scala +++ b/docs/src/main/scala/04-Program.scala @@ -21,7 +21,7 @@ import ldbc.dsl.io.* // #program val program: Executor[IO, Int] = - sql"INSERT INTO task (name, done) VALUES ('task 1', false)".update + sql"INSERT INTO user (name, email) VALUES ('Carol', 'carol@example.com')".update // #program // #connection @@ -29,7 +29,8 @@ import ldbc.dsl.io.* host = "127.0.0.1", port = 13306, user = "ldbc", - password = Some("password") + password = Some("password"), + ssl = SSL.Trusted ) // #connection From 9bcc932fa0d1c5d92c19ed42d850f7fe38c87379 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 15:37:35 +0900 Subject: [PATCH 073/160] Create directory.conf --- docs/src/main/mdoc/directory.conf | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/src/main/mdoc/directory.conf diff --git a/docs/src/main/mdoc/directory.conf b/docs/src/main/mdoc/directory.conf new file mode 100644 index 000000000..9a5a21089 --- /dev/null +++ b/docs/src/main/mdoc/directory.conf @@ -0,0 +1,4 @@ +laika.navigationOrder = [ + index.md + Migration-Notes.md +] From 78258755e5f1a5deed52e2ad64b9f4dfc4bf0a56 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 15:37:54 +0900 Subject: [PATCH 074/160] Delete unused --- docs/src/main/mdoc/index.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md index 0dd9cc75a..b28f5e2f0 100644 --- a/docs/src/main/mdoc/index.md +++ b/docs/src/main/mdoc/index.md @@ -55,45 +55,35 @@ For people that want to skip the explanations and see it action, this is the pla ### Dependency Configuration -@@@ vars ```scala libraryDependencies += "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@" ``` -@@@ For Cross-Platform projects (JVM, JS, and/or Native): -@@@ vars ```scala libraryDependencies += "@ORGANIZATION@" %%% "ldbc-dsl" % "@VERSION@" ``` -@@@ The dependency package used depends on whether the database connection is made via a connector using the Java API or a connector provided by ldbc. **Use jdbc connector** -@@@ vars ```scala libraryDependencies += "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@" ``` -@@@ **Use ldbc connector** -@@@ vars ```scala libraryDependencies += "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@" ``` -@@@ For Cross-Platform projects (JVM, JS, and/or Native) -@@@ vars ```scala libraryDependencies += "@ORGANIZATION@" %%% "ldbc-connector" % "@VERSION@" ``` -@@@ ### Usage From 79962cc703b23e58bb332f99f91635c49aa68cce Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 15:58:46 +0900 Subject: [PATCH 075/160] Fixed Setup document --- docs/src/main/mdoc/ja/tutorial/Setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/mdoc/ja/tutorial/Setup.md b/docs/src/main/mdoc/ja/tutorial/Setup.md index df65cabc9..d53dc564e 100644 --- a/docs/src/main/mdoc/ja/tutorial/Setup.md +++ b/docs/src/main/mdoc/ja/tutorial/Setup.md @@ -9,7 +9,7 @@ ## データベースセットアップ -まず、データベースを起動します。以下のコードを使用して、データベースを起動します。 +まず、Dockerを使用してデータベースを起動します。以下のコードを使用して、データベースを起動します。 ```yaml version: '3' From 8e38300e36a6953d64abba576868a27e1ba5cf6a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 15:59:01 +0900 Subject: [PATCH 076/160] Fixed tutorial index document --- docs/src/main/mdoc/ja/tutorial/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/mdoc/ja/tutorial/index.md b/docs/src/main/mdoc/ja/tutorial/index.md index 64bc9da51..4ea5ffb27 100644 --- a/docs/src/main/mdoc/ja/tutorial/index.md +++ b/docs/src/main/mdoc/ja/tutorial/index.md @@ -1,13 +1,13 @@ {% - laika.title = Intro + laika.title = はじめに laika.metadata.language = ja %} -# Tutorial +# チュートリアル -This section contains instructions and examples to get you started. It's written in tutorial style, intended to be read start to finish. +このセクションは、あなたが始めるための説明と例を含んでいます。チュートリアル形式で書かれており、最初から最後まで読むことを想定しています。 -## Table of Contents +## 目次 @:navigationTree { entries = [ { target = "/ja/tutorial", depth = 2 } ] From cdceed90e3644683ef74b65e725918361d8096d3 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:00:48 +0900 Subject: [PATCH 077/160] Create Simple-Program tutorial document --- .../main/mdoc/ja/tutorial/Simple-Program.md | 106 ++++++++++++++++++ docs/src/main/mdoc/ja/tutorial/directory.conf | 1 + 2 files changed, 107 insertions(+) create mode 100644 docs/src/main/mdoc/ja/tutorial/Simple-Program.md diff --git a/docs/src/main/mdoc/ja/tutorial/Simple-Program.md b/docs/src/main/mdoc/ja/tutorial/Simple-Program.md new file mode 100644 index 000000000..0896f67f4 --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/Simple-Program.md @@ -0,0 +1,106 @@ +{% + laika.title = シンプルプログラム + laika.metadata.language = ja +%} + +# シンプルなプログラム + +ここではまず、シンプルなプログラムを作成し実行することでldbcの基本的な使い方を説明します。 + +※ ここで使用するプログラムの環境はセットアップで構築したものを前提としています。 + +## 1つめのプログラム + +このプログラムでは、データベースに接続し計算結果を取得するプログラムを作成します。 + +それでは、sql string interpolatorを使って、データベースに定数の計算を依頼する問い合わせを作成してみましょう。 + +```scala +val program: Executor[IO, Option[Int]] = sql"SELECT 2".query[Int].to[Option] +``` + +sql string interpolatorを使って作成したクエリは`query`メソッドで取得する型の決定を行います。ここでは`Int`型を取得するため、`query[Int]`としています。また、`to`メソッドで取得する型を決定します。ここでは`Option`型を取得するため、`to[Option]`としています。 + +| Method | Return Type | Notes | +|--------------|----------------|-------------------------------| +| `to[List]` | `F[List[A]]` | `すべての結果をリストで表示` | +| `to[Option]` | `F[Option[A]]` | `結果は0か1、そうでなければエラーが発生する` | +| `unsafe` | `F[A]` | `正確には1つの結果で、そうでない場合はエラーが発生する` | + +最後に、データベースに接続して値を返すプログラムを書きます。このプログラムは、データベースに接続し、クエリを実行し、結果を取得します。 + +```scala +connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() +``` + +定数を計算するためにデータベースに接続した。かなり印象的だ。 + +**Scala CLIで実行** + +このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` + +## 2つめのプログラム + +一つの取引で複数のことをしたい場合はどうすればいいのか?簡単だ!Executorはモナドなので、for内包を使って2つの小さなプログラムを1つの大きなプログラムにすることができる。 + +```scala +val program: Executor[IO, (List[Int], Option[Int], Int)] = + for + result1 <- sql"SELECT 1".query[Int].to[List] + result2 <- sql"SELECT 2".query[Int].to[Option] + result3 <- sql"SELECT 3".query[Int].unsafe + yield (result1, result2, result3) +``` + +最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 + +```scala +connection + .use { conn => + program.readOnly(conn).map(println(_)) + } + .unsafeRunSync() +``` + +**Scala CLIで実行** + +このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` + +## 3つめのプログラム + +データベースに対して書き込みを行うプログラムを書いてみよう。ここでは、データベースに接続し、クエリを実行し、データを挿入する。 + +```scala +val program: Executor[IO, Int] = + sql"INSERT INTO user (name, email) VALUES ('Carol', 'carol@example.com')".update +``` + +先ほどと異なる点は、`commit`メソッドを呼び出すことである。これにより、トランザクションがコミットされ、データベースにデータが挿入される。 + +```scala +connection + .use { conn => + program.commit(conn).map(println(_)) + } + .unsafeRunSync() +``` + +**Scala CLIで実行** + +このプログラムも、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 + +```shell +scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ +``` diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index e810d7618..d28209804 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -3,4 +3,5 @@ laika.navigationOrder = [ index.md, Setup.md, Connection.md, + Simple-Program.md ] From deb82ae412ce325b833368f47ea3327cd888cdd6 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:12:41 +0900 Subject: [PATCH 078/160] Create Database-Operations tutorial document --- .../mdoc/ja/tutorial/Database-Operations.md | 53 +++++++++++++++++++ docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/mdoc/ja/tutorial/Database-Operations.md diff --git a/docs/src/main/mdoc/ja/tutorial/Database-Operations.md b/docs/src/main/mdoc/ja/tutorial/Database-Operations.md new file mode 100644 index 000000000..17c04c0f0 --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/Database-Operations.md @@ -0,0 +1,53 @@ +{% + laika.title = データベース操作 + laika.metadata.language = ja +%} + +# データベース操作 + +このセクションでは、データベース操作について説明します。 + +データベース接続を行う前にコミットのタイミングや読み書き専用などの設定を行う必要があります。 + +## 読み取り専用 + +読み取り専用のトランザクションを開始するには、`readOnly`メソッドを使用します。 + +`readOnly`メソッドを使用することで実行するクエリの処理を読み込み専用にすることができます。`readOnly`メソッドは`insert/update/delete`文でも使用することができますが、書き込み処理を行うので実行時にエラーとなります。 + +```scala +val read = sql"SELECT 1".query[Int].to[Option].readOnly(connection) +``` + +## 書き込み + +書き込みを行うには、`commit`メソッドを使用します。 + +`commit`メソッドを使用することで実行するクエリの処理をクエリ実行時ごとにコミットするように設定することができます。 + +```scala +val write = sql"INSERT INTO `table`(`c1`, `c2`) VALUES ('column 1', 'column 2')".update.commit(connection) +``` + +## トランザクション + +トランザクションを開始するには、`transaction`メソッドを使用します。 + +`transaction`メソッドを使用することで複数のデータベース接続処理を1つのトランザクションにまとめることができます。 + +ldbcは`Executor[F, A]`という形式でデータベースへの接続処理を組むことになる。 Executorはモナドなので、for内包を使って2つの小さなプログラムを1つの大きなプログラムにすることができる。 + +```scala +val program: Executor[IO, (List[Int], Option[Int], Int)] = + for + result1 <- sql"SELECT 1".query[Int].to[List] + result2 <- sql"SELECT 2".query[Int].to[Option] + result3 <- sql"SELECT 3".query[Int].unsafe + yield (result1, result2, result3) +``` + +1つのプログラムとなった`Executor`を`transaction`メソッドで1つのトランザクションでまとめて処理を行うことができます。 + +```scala +val transaction = program.transaction(connection) +``` diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index d28209804..9fe1f4cf1 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -3,5 +3,6 @@ laika.navigationOrder = [ index.md, Setup.md, Connection.md, - Simple-Program.md + Simple-Program.md, + Database-Operations.md ] From 351f18fb36f2bd1375e787cb7e64117fbc9b1dd8 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:12:51 +0900 Subject: [PATCH 079/160] Fixed variables --- docs/src/main/mdoc/ja/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/main/mdoc/ja/index.md b/docs/src/main/mdoc/ja/index.md index 02302991d..898ecf6a1 100644 --- a/docs/src/main/mdoc/ja/index.md +++ b/docs/src/main/mdoc/ja/index.md @@ -50,15 +50,15 @@ ldbcは、型付けされた純粋な関数型プログラミングに興味が libraryDependencies ++= Seq( // まずはこの1つから - "$org$" %% "ldbc-dsl" % "$version$", + "@ORGANIZATION@" %% "ldbc-dsl" % "@VERSION@", // 使用するコネクタを選択 - "$org$" %% "jdbc-connector" % "$version$", // Javaコネクタ (対応プラットフォーム: JVM) - "$org$" %% "ldbc-connector" % "$version$", // Scalaコネクタ (対応プラットフォーム: JVM, JS, Native) + "@ORGANIZATION@" %% "jdbc-connector" % "@VERSION@", // Javaコネクタ (対応プラットフォーム: JVM) + "@ORGANIZATION@" %% "ldbc-connector" % "@VERSION@", // Scalaコネクタ (対応プラットフォーム: JVM, JS, Native) // そして、必要に応じてこれらを加える - "$org$" %% "ldbc-query-builder" % "$version$", // 型安全なクエリ構築 - "$org$" %% "ldbc-schema" % "$version$", // データベーススキーマの構築 + "@ORGANIZATION@" %% "ldbc-query-builder" % "@VERSION@", // 型安全なクエリ構築 + "@ORGANIZATION@" %% "ldbc-schema" % "@VERSION@", // データベーススキーマの構築 ) ``` From 247fa289681d5902bdaae0dbcb11cbb57ed2f924 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:22:48 +0900 Subject: [PATCH 080/160] Create Parameterized-Queries tutorial document --- .../ja/tutorial/Parameterized-Queries.md} | 69 ++++++++----------- docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 2 files changed, 31 insertions(+), 41 deletions(-) rename docs/{old/04-Parameterized-Queries.md => src/main/mdoc/ja/tutorial/Parameterized-Queries.md} (55%) diff --git a/docs/old/04-Parameterized-Queries.md b/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md similarity index 55% rename from docs/old/04-Parameterized-Queries.md rename to docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md index 4947392d5..8c4b7df91 100644 --- a/docs/old/04-Parameterized-Queries.md +++ b/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md @@ -1,63 +1,52 @@ +{% + laika.title = パラメータ + laika.metadata.language = ja +%} + # パラメータ化されたクエリ この章では、パラメータ化されたクエリを構築する方法を学びます。 -使用するテーブルは以下の通りです。 - -```sql -CREATE TABLE country ( - code character(3) NOT NULL, - name text NOT NULL, - population integer NOT NULL, - gnp numeric(10,2) - -- more columns, but we won't use them here -) -``` - -```scala -case class Country(code: String, name: String, population: Int, gnp: Option[Double]) -``` - ## パラメータの追加 まずは、パラメーターを持たないクエリを作成します。 ```scala -sql"SELECT code, name, population, gnp FROM country".query[Country].to[List] +sql"SELECT name, email FROM user".query[(String, String)].to[List] ``` -次にクエリをメソッドに組み込んで、ユーザーが指定する国コードと一致するデータのみを選択するパラメーターを追加してみましょう。文字列の補間を行うのと同じように、code引数を$codeとしてSQL文に挿入します。 +次にクエリをメソッドに組み込んで、ユーザーが指定する`id`と一致するデータのみを選択するパラメーターを追加してみましょう。文字列の補間を行うのと同じように、`id`引数を`$id`としてSQL文に挿入します。 ```scala -val code = "JPN" +val id = 1 -sql"SELECT code, name, population, gnp FROM country WHERE code = $code".query[Country].to[List] +sql"SELECT name, email FROM user WHERE id = $id".query[(String, String)].to[List] ``` コネクションを使用してクエリを実行すると問題なく動作します。 ```scala connection.use { conn => - sql"SELECT code, name, population, gnp FROM country WHERE code = $code" - .query[Country] + sql"SELECT name, email FROM user WHERE id = $id" + .query[(String, String)] .to[List] .readOnly(conn) } ``` -ここでは何が起こっているのでしょうか?文字列リテラルをSQL文字列にドロップしているだけのように見えますが、実際にはPreparedStatementを構築しており、code値は最終的にsetStringの呼び出しによって設定されます +ここでは何が起こっているのでしょうか?文字列リテラルをSQL文字列にドロップしているだけのように見えますが、実際には`PreparedStatement`を構築しており、`id`値は最終的に`setInt`の呼び出しによって設定されます。 ## 複数のパラメータ 複数のパラメータも同じように機能する。驚きはない。 ```scala -val code = "JPN" -val population = 100000000 +val id = 1 +val email = "alice@example.com" connection.use { conn => - sql"SELECT code, name, population, gnp FROM country WHERE code = $code AND population > $population" - .query[Country] + sql"SELECT name, email FROM user WHERE id = $id AND email > $email" + .query[(String, String)] .to[List] .readOnly(conn) } @@ -68,17 +57,17 @@ connection.use { conn => SQLリテラルを扱う際によくあるイラつきは、一連の引数をIN句にインライン化したいという欲求ですが、SQLはこの概念をサポートしていません(JDBCも何もサポートしていません)。 ```scala -val codes = NonEmptyList.of("JPN", "USA", "FRA") +val ids = NonEmptyList.of(1, 2, 3) connection.use { conn => - sql"SELECT code, name, population, gnp FROM country WHERE" ++ in("code", codes) - .query[Country] - .to[List] - .readOnly(conn) + sql"SELECT name, email FROM user WHERE" ++ in("id", ids) + .query[(String, String)] + .to[List] + .readOnly(conn) } ``` -IN句は空であってはならないので、コードはNonEmptyListであることに注意。 +IN句は空であってはならないので、`ids`は`NonEmptyList`であることに注意。 このクエリーを実行すると、望ましい結果が得られる @@ -101,23 +90,23 @@ ldbcでは他にもいくつかの便利な関数が用意されています。 例えば受け取った値に応じて取得するカラムを変更する場合、以下のように記述できます。 ```scala -val column = "code" +val column = "name" -sql"SELECT $column FROM country".query[String].to[List] +sql"SELECT $column FROM user".query[String].to[List] ``` -動的なパラメーターはPreparedStatementによって処理が行われるため、クエリ文字列自体は`?`で置き換えられます。 +動的なパラメーターは`PreparedStatement`によって処理が行われるため、クエリ文字列自体は`?`で置き換えられます。 -そのため、このクエリは`SELECT ? FROM country`として実行されます。 +そのため、このクエリは`SELECT ? FROM user`として実行されます。 これではログに出力されるクエリがわかりにくいため、`$column`は静的な値として扱いたい場合は、`$column`を`${sc(column)}`とすることで、クエリ文字列に直接埋め込まれるようになります。 ```scala -val column = "code" +val column = "name" -sql"SELECT ${sc(column)} FROM country".query[String].to[List] +sql"SELECT ${sc(column)} FROM user".query[String].to[List] ``` -このクエリは`SELECT code FROM country`として実行されます。 +このクエリは`SELECT name FROM user`として実行されます。 > `sc(...)`は渡された文字列のエスケープを行わないことに注意してください。ユーザから与えられたデータを渡すことは、インジェクションのリスクになります。 diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 9fe1f4cf1..9f954d95c 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -4,5 +4,6 @@ laika.navigationOrder = [ Setup.md, Connection.md, Simple-Program.md, - Database-Operations.md + Database-Operations.md, + Parameterized-Queries.md ] From 88fa54d263af2ab258b0d9a9e157a2ad364b143c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:27:48 +0900 Subject: [PATCH 081/160] Create Selecting-Data tutorial document --- docs/old/05-Selecting-Data.md | 85 ------------------- .../main/mdoc/ja/tutorial/Selecting-Data.md | 55 ++++++++++++ docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 3 files changed, 57 insertions(+), 86 deletions(-) delete mode 100644 docs/old/05-Selecting-Data.md create mode 100644 docs/src/main/mdoc/ja/tutorial/Selecting-Data.md diff --git a/docs/old/05-Selecting-Data.md b/docs/old/05-Selecting-Data.md deleted file mode 100644 index a275ce7f0..000000000 --- a/docs/old/05-Selecting-Data.md +++ /dev/null @@ -1,85 +0,0 @@ -# データ選択 - -この章では、ldbcデータセットを使用してデータを選択する方法を説明します。まず、データベースをセットアップします。以下のコードを使用して、MySQLデータベースをセットアップします。 - -@@@ vars -```yaml -version: '3' -services: - mysql: - image: mysql:"$mysqlVersion$" - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 -``` -@@@ - -次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 - - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -@@@ vars -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} -``` -@@@ - -## コレクションへの行の読み込み - -最初のクエリでは、低レベルのクエリを目指して、いくつかの国名をリストに選択し、最初の数件をプリントアウトしてみましょう。ここにはいくつかのステップがあるので、途中のタイプを記しておきます。 - -```scala -sql"SELECT name FROM country" - .query[String] // Query[IO, String] - .to[List] // Executor[IO, List[String]] - .readOnly(conn) // IO[List[String]] - .unsafeRunSync() // List[String] - .foreach(println) // Unit -``` - -これを少し分解してみよう。 - -- `sql"SELECT name FROM country".query[String]`は`Query[IO, String]`を定義し、返される各行をStringにマップする1列のクエリです。このクエリは1列のクエリで、返される行をそれぞれStringにマップします。 -- `.to[List]`は、行をリストに蓄積する便利なメソッドで、この場合は`Executor[IO, List[String]]`を生成します。このメソッドは、CanBuildFromを持つすべてのコレクション・タイプで動作します。 -- `readOnly(conn)`は`IO[List[String]]`を生成し、これを実行すると通常のScala List[String]が出力される。 -- `unsafeRunSync()`は、IOモナドを実行し、結果を取得する。これは、IOモナドを実行し、結果を取得するために使用される。 -- `foreach(println)`は、リストの各要素をプリントアウトする。 - -## 複数列クエリ - -もちろん、複数のカラムを選択してタプルにマッピングすることもできます。 - -```scala -sql"SELECT name, population FROM country" - .query[(String, Int)] // Query[IO, (String, Int)] - .to[List] // Executor[IO, List[(String, Int)]] - .readOnly(conn) // IO[List[(String, Int)]] - .unsafeRunSync() // List[(String, Int)] - .foreach(println) // Unit -``` - -ldbcは、複数のカラムを選択してクラスにマッピングすることもできます。 - -```scala -case class Country(name: String, population: Int) - -sql"SELECT name, population FROM country" - .query[Country] // Query[IO, Country] - .to[List] // Executor[IO, List[Country]] - .readOnly(conn) // IO[List[Country]] - .unsafeRunSync() // List[Country] - .foreach(println) // Unit -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md new file mode 100644 index 000000000..34dc4c504 --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/Selecting-Data.md @@ -0,0 +1,55 @@ +{% + laika.title = データ選択 + laika.metadata.language = ja +%} + +# データ選択 + +この章では、ldbcデータセットを使用してデータを選択する方法を説明します。 + +## コレクションへの行の読み込み + +最初のクエリでは、低レベルのクエリを目指して、いくつかのユーザーをリストに選択し、最初の数件をプリントアウトしてみましょう。ここにはいくつかのステップがあるので、途中のタイプを記しておきます。 + +```scala +sql"SELECT name FROM user" + .query[String] // Query[IO, String] + .to[List] // Executor[IO, List[String]] + .readOnly(conn) // IO[List[String]] + .unsafeRunSync() // List[String] + .foreach(println) // Unit +``` + +これを少し分解してみよう。 + +- `sql"SELECT name FROM user".query[String]`は`Query[IO, String]`を定義し、返される各行をStringにマップする1列のクエリです。このクエリは1列のクエリで、返される行をそれぞれStringにマップします。 +- `.to[List]`は、行をリストに蓄積する便利なメソッドで、この場合は`Executor[IO, List[String]]`を生成します。このメソッドは、CanBuildFromを持つすべてのコレクション・タイプで動作します。 +- `readOnly(conn)`は`IO[List[String]]`を生成し、これを実行すると通常のScala `List[String]`が出力される。 +- `unsafeRunSync()`は、IOモナドを実行し、結果を取得する。これは、IOモナドを実行し、結果を取得するために使用される。 +- `foreach(println)`は、リストの各要素をプリントアウトする。 + +## 複数列クエリ + +もちろん、複数のカラムを選択してタプルにマッピングすることもできます。 + +```scala +sql"SELECT name, email FROM user" + .query[(String, String)] // Query[IO, (String, String)] + .to[List] // Executor[IO, List[(String, String)]] + .readOnly(conn) // IO[List[(String, String)]] + .unsafeRunSync() // List[(String, String)] + .foreach(println) // Unit +``` + +ldbcは、複数のカラムを選択してクラスにマッピングすることもできます。 + +```scala +case class User(name: String, population: Int) + +sql"SELECT name, email FROM user" + .query[User] // Query[IO, User] + .to[List] // Executor[IO, List[User]] + .readOnly(conn) // IO[List[User]] + .unsafeRunSync() // List[User] + .foreach(println) // Unit +``` diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 9f954d95c..5060377ad 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -5,5 +5,6 @@ laika.navigationOrder = [ Connection.md, Simple-Program.md, Database-Operations.md, - Parameterized-Queries.md + Parameterized-Queries.md, + Selecting-Data.md ] From 0df3a448271258201ed93955735a5eecfa314ebf Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:40:26 +0900 Subject: [PATCH 082/160] Create Updating-Data tutorial document --- docs/old/06-Updating-Data.md | 146 ------------------ .../main/mdoc/ja/tutorial/Updating-Data.md | 115 ++++++++++++++ docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 3 files changed, 117 insertions(+), 147 deletions(-) delete mode 100644 docs/old/06-Updating-Data.md create mode 100644 docs/src/main/mdoc/ja/tutorial/Updating-Data.md diff --git a/docs/old/06-Updating-Data.md b/docs/old/06-Updating-Data.md deleted file mode 100644 index 783010555..000000000 --- a/docs/old/06-Updating-Data.md +++ /dev/null @@ -1,146 +0,0 @@ -# データ更新 - -この章では、データベースのデータを変更する操作と、更新結果を取得する方法について説明します。 - -@@@ vars -```yaml -version: '3' -services: - mysql: - image: mysql:"$mysqlVersion$" - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 -``` -@@@ - -次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 - - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -@@@ vars -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} -``` -@@@ - -## 挿入 - -挿入は簡単で、selectと同様に動作します。ここでは、`task`テーブルに行を挿入するExecutorを作成するメソッドを定義します。 - -```scala -def insertTask(name: String, done: Boolean): Executor[IO, Int] = - sql"INSERT INTO task (name, done) VALUES ($name, $done)" - .update // Executor[IO, Int] -``` - -いくつかの行を挿入してみよう。 - -```scala -insertTask("task1", done = false).commit.unsafeRunSync() -insertTask("task2", done = true).commit.unsafeRunSync() -insertTask("task3", done = false).commit.unsafeRunSync() -``` - -そして読み返す。 - -```scala -sql"SELECT * FROM task" - .query[(Int, String, Boolean)] // Query[IO, (Int, String, Boolean)] - .to[List] // Executor[IO, List[(Int, String, Boolean)]] - .readOnly(conn) // IO[List[(Int, String, Boolean)]] - .unsafeRunSync() // List[(Int, String, Boolean)] - .foreach(println) // Unit -// (1,task1,false) -// (2,task2,true) -// (3,task3,false) -``` - -## 更新 - -更新も同じパターンだ。ここではタスクを完了済みに更新する。 - -```scala -def updateTaskDone(id: Int): Executor[IO, Int] = - sql"UPDATE task SET done = ${true} WHERE id = $id" - .update // Executor[IO, Int] -``` - -結果の取得 - -```scala -updateTaskDone(1).commit.unsafeRunSync() - -sql"SELECT * FROM task WHERE id = 1" - .query[(Int, String, Boolean)] // Query[IO, (Int, String, Boolean)] - .to[Option] // Executor[IO, List[(Int, String, Boolean)]] - .readOnly(conn) // IO[List[(Int, String, Boolean)]] - .unsafeRunSync() // List[(Int, String, Boolean)] - .foreach(println) // Unit -// Some((1,task1,true)) -``` - -## 自動生成キー - -インサートする際には、新しく生成されたキーを返したいものです。まず、挿入して最後に生成されたキーを`LAST_INSERT_ID`で取得し、指定された行を選択するという難しい方法をとります。 - -```scala -def insertTask(name: String, done: Boolean): Executor[IO, (Int, String, Boolean)] = - for { - _ <- sql"INSERT INTO task (name, done) VALUES ($name, $done)".update - id <- sql"SELECT LAST_INSERT_ID()".query[Int].unsafe - task <- sql"SELECT * FROM task WHERE id = $id".query[(Int, String, Boolean)].to[Option] - } yield task -``` - -```scala -insertTask("task4", done = false).commit.unsafeRunSync() -``` - -これは苛立たしいことだが、すべてのデータベースでサポートされている(ただし、「最後に使用されたIDを取得する」機能はベンダーによって異なる)。 - -MySQLでは、`AUTO_INCREMENT`が設定された行のみが挿入時に返すことができます。上記の操作を2つのステートメントに減らすことができます - -自動生成キーを使用して行を挿入する場合、`returning`メソッドを使用して自動生成キーを取得できます。 - -```scala -def insertTask(name: String, done: Boolean): Executor[IO, (Int, String, Boolean)] = - for { - id <- sql"INSERT INTO task (name, done) VALUES ($name, $done)".returning[Long] - task <- sql"SELECT * FROM task WHERE id = $id".query[(Int, String, Boolean)].to[Option] - } yield task -``` - -```scala -insertTask("task5", done = false).commit.unsafeRunSync() -``` - -## バッチ更新 - -バッチ更新を行うには、`NonEmptyList`を使用して複数の行を挿入する`insertManyTask`メソッドを定義します。 - -```scala -def insertManyTask(tasks: NonEmptyList[(String, Boolean)]): Executor[IO, Int] = { - val value = tasks.map { case (name, done) => sql"($name, $done)" } - (sql"INSERT INTO task (name, done) VALUES" ++ values(value)).update -} -``` - -このプログラムを実行すると、更新された行数が得られる。 - -```scala -insertManyTask(NonEmptyList.of(("task6", false), ("task7", true), ("task8", false))).commit.unsafeRunSync() -``` diff --git a/docs/src/main/mdoc/ja/tutorial/Updating-Data.md b/docs/src/main/mdoc/ja/tutorial/Updating-Data.md new file mode 100644 index 000000000..b7b8ac1ad --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/Updating-Data.md @@ -0,0 +1,115 @@ +{% + laika.title = データ更新 + laika.metadata.language = ja +%} + +# データ更新 + +この章では、データベースのデータを変更する操作と、更新結果を取得する方法について説明します。 + +## 挿入 + +挿入は簡単で、selectと同様に動作します。ここでは、`user`テーブルに行を挿入する`Executor`を作成するメソッドを定義します。 + +```scala +def insertUser(name: String, email: String): Executor[IO, Int] = + sql"INSERT INTO user (name, email) VALUES ($name, $email)" + .update +``` + +行を挿入してみよう。 + +```scala +insertUser("dave", "dave@example.com").commit.unsafeRunSync() +``` + +そして読み返す。 + +```scala +sql"SELECT * FROM user" + .query[(Int, String, String)] // Query[IO, (Int, String, String)] + .to[List] // Executor[IO, List[(Int, String, String)]] + .readOnly(conn) // IO[List[(Int, String, String)]] + .unsafeRunSync() // List[(Int, String, String)] + .foreach(println) // Unit +``` + +## 更新 + +更新も同じパターンだ。ここではユーザーのメールアドレスを更新する。 + +```scala +def updateUserEmail(id: Int, email: String): Executor[IO, Int] = + sql"UPDATE user SET email = $email WHERE id = $id" + .update +``` + +結果の取得 + +```scala +updateUserEmail(1, "alice+1@example.com").commit.unsafeRunSync() + +sql"SELECT * FROM user WHERE id = 1" + .query[(Int, String, String)] // Query[IO, (Int, String, String)] + .to[Option] // Executor[IO, List[(Int, String, String)]] + .readOnly(conn) // IO[List[(Int, String, String)]] + .unsafeRunSync() // List[(Int, String, String)] + .foreach(println) // Unit +// Some((1,alice,alice+1@example.com)) +``` + +## 自動生成キー + +インサートする際には、新しく生成されたキーを返したいものです。まず、挿入して最後に生成されたキーを`LAST_INSERT_ID`で取得し、指定された行を選択するという難しい方法をとります。 + +```scala +def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = + for + _ <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".update + id <- sql"SELECT LAST_INSERT_ID()".query[Int].unsafe + task <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] + yield task +``` + +```scala +insertUser("eve", "eve@example.com").commit.unsafeRunSync() +``` + +これは苛立たしいことだが、すべてのデータベースでサポートされている(ただし、「最後に使用されたIDを取得する」機能はベンダーによって異なる)。 + +MySQLでは、`AUTO_INCREMENT`が設定された行のみが挿入時に返すことができます。上記の操作を2つのステートメントに減らすことができます + +自動生成キーを使用して行を挿入する場合、`returning`メソッドを使用して自動生成キーを取得できます。 + +```scala +def insertUser(name: String, email: String): Executor[IO, (Int, String, String)] = + for + id <- sql"INSERT INTO user (name, email) VALUES ($name, $email)".returning[Int] + user <- sql"SELECT * FROM user WHERE id = $id".query[(Int, String, String)].to[Option] + yield user +``` + +```scala +insertUser("frank", "frank@example.com").commit.unsafeRunSync() +``` + +## バッチ更新 + +バッチ更新を行うには、`NonEmptyList`を使用して複数の行を挿入する`insertManyUser`メソッドを定義します。 + +```scala +def insertManyUser(users: NonEmptyList[(String, String)]): Executor[IO, Int] = + val value = users.map { case (name, email) => sql"($name, $email)" } + (sql"INSERT INTO user (name, email) VALUES" ++ values(value)).update +``` + +このプログラムを実行すると、更新された行数が得られる。 + +```scala +val users = NonEmptyList.of( + ("greg", "greg@example.com"), + ("henry", "henry@example.com") +) + +insertManyUser(users).commit.unsafeRunSync() +``` diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 5060377ad..897d67544 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -6,5 +6,6 @@ laika.navigationOrder = [ Simple-Program.md, Database-Operations.md, Parameterized-Queries.md, - Selecting-Data.md + Selecting-Data.md, + Updating-Data.md ] From cd77fe2b03bfe34fd7ef02376ef1da122aa46429 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:43:11 +0900 Subject: [PATCH 083/160] Create Error-Handling tutorial document --- .../main/mdoc/ja/tutorial/Error-Handling.md} | 15 ++++++++++----- docs/src/main/mdoc/ja/tutorial/directory.conf | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) rename docs/{old/07-Error-Handling.md => src/main/mdoc/ja/tutorial/Error-Handling.md} (69%) diff --git a/docs/old/07-Error-Handling.md b/docs/src/main/mdoc/ja/tutorial/Error-Handling.md similarity index 69% rename from docs/old/07-Error-Handling.md rename to docs/src/main/mdoc/ja/tutorial/Error-Handling.md index 66fe76634..1c2ea1b42 100644 --- a/docs/old/07-Error-Handling.md +++ b/docs/src/main/mdoc/ja/tutorial/Error-Handling.md @@ -1,10 +1,15 @@ +{% + laika.title = エラーハンドリング + laika.metadata.language = ja +%} + # エラーハンドリング この章では、例外をトラップしたり処理したりするプログラムを構築するためのコンビネーター一式を検討する。 ## 例外について -ある操作が成功するかどうかは、ネットワークの健全性、テーブルの現在の内容、ロックの状態など、予測できない要因に依存します。そのため、EitherT[Executor, Throwable, A]のような論理和ですべてを計算するか、明示的に捕捉されるまで例外の伝播を許可するかを決めなければならない。つまり、ldbcのアクション(ターゲット・モナドに変換される)が実行されると、例外が発生する可能性がある。 +ある操作が成功するかどうかは、ネットワークの健全性、テーブルの現在の内容、ロックの状態など、予測できない要因に依存します。そのため、`EitherT[Executor, Throwable, A]`のような論理和ですべてを計算するか、明示的に捕捉されるまで例外の伝播を許可するかを決めなければならない。つまり、ldbcのアクション(ターゲット・モナドに変換される)が実行されると、例外が発生する可能性がある。 発生しやすい例外は主に3種類ある @@ -14,11 +19,11 @@ ## モナド・エラーと派生コンバイネーター -すべてのldbcモナドは、MonadError[?[_], Throwable]を拡張したAsyncインスタンスを提供する。つまり、Executorなどは以下のようなプリミティブな操作を持つことになる +すべてのldbcモナドは、`MonadError[?[_], Throwable]`を拡張したAsyncインスタンスを提供する。つまり、Executorなどは以下のようなプリミティブな操作を持つことになる -- raiseError: 例外を発生させる (ThrowableをM[A]に変換する) -- handleErrorWith: 例外を処理する (M[A]をM[B]に変換する) -- attempt: 例外を捕捉する (M[A]をM[Either[Throwable, A]]に変換する) +- raiseError: 例外を発生させる (Throwableを`M[A]`に変換する) +- handleErrorWith: 例外を処理する (`M[A]`を`M[B]`に変換する) +- attempt: 例外を捕捉する (`M[A]`を`M[Either[Throwable, A]]`に変換する) つまり、どんなldbcプログラムでも`attempt`を加えるだけで例外を捕捉することができるのだ。 diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 897d67544..a179f7e7e 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -7,5 +7,6 @@ laika.navigationOrder = [ Database-Operations.md, Parameterized-Queries.md, Selecting-Data.md, - Updating-Data.md + Updating-Data.md, + Error-Handling.md ] From d235578c004ddd65f4338b01e2ea9e16b1dcfa01 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 16:57:10 +0900 Subject: [PATCH 084/160] Create Logging tutorial document --- .../main/mdoc/ja/tutorial/Logging.md} | 9 +++++++-- docs/src/main/mdoc/ja/tutorial/directory.conf | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) rename docs/{old/08-Logging.md => src/main/mdoc/ja/tutorial/Logging.md} (86%) diff --git a/docs/old/08-Logging.md b/docs/src/main/mdoc/ja/tutorial/Logging.md similarity index 86% rename from docs/old/08-Logging.md rename to docs/src/main/mdoc/ja/tutorial/Logging.md index e3f61bf78..c00a94b33 100644 --- a/docs/old/08-Logging.md +++ b/docs/src/main/mdoc/ja/tutorial/Logging.md @@ -1,6 +1,11 @@ -# ログ +{% + laika.title = ロギング + laika.metadata.language = ja +%} -ldbcではDatabase接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 +# ロギング + +ldbcではデータベース接続の実行ログやエラーログを任意のロギングライブラリを使用して任意の形式で書き出すことができます。 標準ではCats EffectのConsoleを使用したロガーが提供されているため開発時はこちらを使用することができます。 diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index a179f7e7e..3d7168fe3 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -8,5 +8,6 @@ laika.navigationOrder = [ Parameterized-Queries.md, Selecting-Data.md, Updating-Data.md, - Error-Handling.md + Error-Handling.md, + Logging.md ] From 5a0800cebb162156ca6299a6fa88adfadc82542f Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 17:23:08 +0900 Subject: [PATCH 085/160] Create Custom-Data-Type tutorial document --- docs/old/09-Custom-Data-Type.md | 66 ---------------- .../main/mdoc/ja/tutorial/Custom-Data-Type.md | 76 +++++++++++++++++++ docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 3 files changed, 78 insertions(+), 67 deletions(-) delete mode 100644 docs/old/09-Custom-Data-Type.md create mode 100644 docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md diff --git a/docs/old/09-Custom-Data-Type.md b/docs/old/09-Custom-Data-Type.md deleted file mode 100644 index 459c098ad..000000000 --- a/docs/old/09-Custom-Data-Type.md +++ /dev/null @@ -1,66 +0,0 @@ -# カスタム データ型 - -この章では、LDBCで構築したテーブル定義でユーザー独自の型もしくはサポートされていない型を使用するための方法について説明します。 - -@@@ vars -```yaml -version: '3' -services: - mysql: - image: mysql:"$mysqlVersion$" - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 -``` -@@@ - -次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 - - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -@@@ vars -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} -``` -@@@ - -## ユーザー独自の型 - -ldbcではstatementに受け渡す値を`Parameter`で表現しています。`Parameter`はstatementへのバインドする値を表現するためのtraitです。 - -`Parameter`を実装することでstatementに受け渡す値をカスタム型で表現することができます。 - -以下のコード例では、`Parameter`を実装した`CustomParameter`を定義しています。 - -@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #customType } - -@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #customParameter } - -@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #program1 } - -これでstatementにカスタム型をバインドすることができるようになりました。 - -ldbcではパラメーターの他に実行結果から独自の型を取得するための`ResultSetReader`も提供しています。 - -`ResultSetReader`を実装することでstatementの実行結果から独自の型を取得することができます。 - -以下のコード例では、`ResultSetReader`を実装した`CustomResultSetReader`を定義しています。 - -@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #customReader } - -@@snip [05-Program.scala](/docs/src/main/scala/05-Program.scala) { #program2 } - -これでstatementの実行結果からカスタム型を取得することができるようになりました。 diff --git a/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md b/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md new file mode 100644 index 000000000..77ca47563 --- /dev/null +++ b/docs/src/main/mdoc/ja/tutorial/Custom-Data-Type.md @@ -0,0 +1,76 @@ +{% + laika.title = カスタム データ型 + laika.metadata.language = ja +%} + +# カスタム データ型 + +この章では、ldbcで構築したテーブル定義でユーザー独自の型もしくはサポートされていない型を使用するための方法について説明します。 + +セットアップで作成したテーブル定義に新たにカラムを追加します。 + +```sql +ALTER TABLE user ADD COLUMN status BOOLEAN NOT NULL DEFAULT TRUE; +``` + +## 受け渡し + +ldbcではstatementに受け渡す値を`Parameter`で表現しています。`Parameter`はstatementへのバインドする値を表現するためのtraitです。 + +`Parameter`を実装することでstatementに受け渡す値をカスタム型で表現することができます。 + +ユーザー情報にそのユーザーのステータスを表す`Status`を追加します。 + +```scala 3 +enum Status(val done: Boolean, val name: String): + case Active extends TaskStatus(false, "Active") + case InActive extends TaskStatus(true, "InActive") +``` + +以下のコード例では、`Parameter`を実装した`CustomParameter`を定義しています。 + +これによりstatementにカスタム型をバインドすることができるようになります。 + +```scala 3 +given Parameter[Status] with + override def bind[F[_]]( + statement: PreparedStatement[F], + index: Int, + status: Status + ): F[Unit] = + status match + case Status.Active => statement.setBoolean(index, true) + case Status.InActive => statement.setBoolean(index, false) +``` + +カスタム型は他のパラメーターと同じようにstatementにバインドすることができます。 + +```scala +val program1: Executor[IO, Int] = + sql"INSERT INTO user (name, email, status) VALUES (${ "user 1" }, ${ "user@example.com" }, ${ Status.Active })".update +``` + +これでstatementにカスタム型をバインドすることができるようになりました。 + +## 読み取り + +ldbcではパラメーターの他に実行結果から独自の型を取得するための`ResultSetReader`も提供しています。 + +`ResultSetReader`を実装することでstatementの実行結果から独自の型を取得することができます。 + +以下のコード例では、`ResultSetReader`を実装した`CustomResultSetReader`を定義しています。 + +```scala 3 +given ResultSetReader[IO, Status] = + ResultSetReader.mapping[IO, Boolean, Status] { + case true => Status.Active + case false => Status.InActive + } +``` + +```scala 3 +val program2: Executor[IO, (String, String, Status)] = + sql"SELECT name, email, status FROM user WHERE id = 1".query[(String, String, Status)].unsafe +``` + +これでstatementの実行結果からカスタム型を取得することができるようになりました。 diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 3d7168fe3..32ac31baf 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -9,5 +9,6 @@ laika.navigationOrder = [ Selecting-Data.md, Updating-Data.md, Error-Handling.md, - Logging.md + Logging.md, + Custom-Data-Type.md ] From b8103bbf169b13dc0859ffbb19a72b0a0e781c1a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 17:23:20 +0900 Subject: [PATCH 086/160] Fixed 05-Program --- docs/src/main/scala/05-Program.scala | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/src/main/scala/05-Program.scala b/docs/src/main/scala/05-Program.scala index 91f93c433..299210e20 100644 --- a/docs/src/main/scala/05-Program.scala +++ b/docs/src/main/scala/05-Program.scala @@ -24,33 +24,34 @@ import ldbc.dsl.io.* // #given // #customType - enum TaskStatus(val done: Boolean, val name: String): - case Pending extends TaskStatus(false, "Pending") - case Done extends TaskStatus(true, "Done") + enum Status: + case Active, InActive // #customType // #customParameter - given Parameter[TaskStatus] with - override def bind[F[_]](statement: PreparedStatement[F], index: Int, status: TaskStatus): F[Unit] = - statement.setBoolean(index, status.done) + given Parameter[Status] with + override def bind[F[_]](statement: PreparedStatement[F], index: Int, status: Status): F[Unit] = + status match + case Status.Active => statement.setBoolean(index, true) + case Status.InActive => statement.setBoolean(index, false) // #customParameter // #program1 val program1: Executor[IO, Int] = - sql"INSERT INTO task (name, done) VALUES (${ "task 1" }, ${ TaskStatus.Done })".update + sql"INSERT INTO user (name, email, status) VALUES (${ "user 1" }, ${ "user@example.com" }, ${ Status.Active })".update // #program1 // #customReader - given ResultSetReader[IO, TaskStatus] = - ResultSetReader.mapping[IO, Boolean, TaskStatus] { - case true => TaskStatus.Done - case false => TaskStatus.Pending + given ResultSetReader[IO, Status] = + ResultSetReader.mapping[IO, Boolean, Status] { + case true => Status.Active + case false => Status.InActive } // #customReader // #program2 - val program2: Executor[IO, (String, TaskStatus)] = - sql"SELECT name, done FROM task WHERE id = 1".query[(String, TaskStatus)].unsafe + val program2: Executor[IO, (String, String, Status)] = + sql"SELECT name, email, status FROM user WHERE id = 1".query[(String, String, Status)].unsafe // #program2 // #connection @@ -58,7 +59,8 @@ import ldbc.dsl.io.* host = "127.0.0.1", port = 13306, user = "ldbc", - password = Some("password") + password = Some("password"), + ssl = SSL.Trusted ) // #connection @@ -68,5 +70,4 @@ import ldbc.dsl.io.* program1.commit(conn) *> program2.readOnly(conn).map(println(_)) } .unsafeRunSync() - // ("task 1", Done) // #run From a2e20512481ec4ef9f1bbda32f4e6606f71cb540 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 17:48:47 +0900 Subject: [PATCH 087/160] Create Query-Builder tutorial document --- .../main/mdoc/ja/tutorial/Query-Builder.md} | 271 ++++++++---------- docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 2 files changed, 121 insertions(+), 153 deletions(-) rename docs/{old/10-Query-Builder.md => src/main/mdoc/ja/tutorial/Query-Builder.md} (52%) diff --git a/docs/old/10-Query-Builder.md b/docs/src/main/mdoc/ja/tutorial/Query-Builder.md similarity index 52% rename from docs/old/10-Query-Builder.md rename to docs/src/main/mdoc/ja/tutorial/Query-Builder.md index 87104830e..7f054e60a 100644 --- a/docs/old/10-Query-Builder.md +++ b/docs/src/main/mdoc/ja/tutorial/Query-Builder.md @@ -1,66 +1,32 @@ +{% + laika.title = クエリビルダー + laika.metadata.language = ja +%} + # クエリビルダー この章では、型安全にクエリを構築するための方法について説明します。 プロジェクトに以下の依存関係を設定する必要があります。 -@@@ vars ```scala -libraryDependencies += "$org$" %% "ldbc-query-builder" % "$version$" -``` -@@@ - -@@@ vars -```yaml -version: '3' -services: - mysql: - image: mysql:"$mysqlVersion$" - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 +//> using dep "@ORGANIZATION@::ldbc-query-builder:@VERSION@" ``` -@@@ - -次に、データベースの初期化を行います。以下のコードを使用して、データベースに接続し必要なテーブルを作成します。 - - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -@@@ vars -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:${version} --dependency io.github.takapi327::ldbc-connector:${version} -``` -@@@ ldbcでは、クラスを使用してクエリを構築します。 -```scala +```scala 3 import ldbc.query.builder.* -case class Task(id: Int, name: String, done: Boolean) derives Table +case class User(id: Int, name: String, email: String) derives Table ``` -`Task`クラスは`Table`トレイトを継承しています。`Table`トレイトは`Table`クラスを継承しているため、`Table`クラスのメソッドを使用してクエリを構築することができます。 +`User`クラスは`Table`トレイトを継承しています。`Table`トレイトは`Table`クラスを継承しているため、`Table`クラスのメソッドを使用してクエリを構築することができます。 ```scala -val query = Table[Task] - .select(task => (task.id, task.name, task.done)) - .where(_.done === true) - .orderBy(_.id.asc) - .limit(1) +val query = Table[User] + .select(user => (user.id, user.name, user.email)) + .where(_.email === "alice@example.com") ``` ## SELECT @@ -70,37 +36,33 @@ val query = Table[Task] 特定のカラムのみ取得を行うSELECT文を構築するには`select`メソッドで取得したいカラムを指定するだけです。 ```scala -val select = Table[Task] - .select(_.id) +val select = Table[User].select(_.id) -select.statement === "SELECT id FROM task" +select.statement === "SELECT id FROM user" ``` 複数のカラムを指定する場合は`select`メソッドで取得したいカラムを指定して指定したカラムのタプルを返すだけです。 ```scala -val select = Table[Task] - .select(task => (task.id, task.name)) +val select = Table[User].select(user => (user.id, user.name)) -select.statement === "SELECT id, name FROM task" +select.statement === "SELECT id, name FROM user" ``` 全てのカラムを指定したい場合はTableが提供する`selectAll`メソッドを使用することで構築できます。 ```scala -val select = Table[Task] - .selectAll +val select = Table[User].selectAll -select.statement === "SELECT id, name, done FROM task" +select.statement === "SELECT id, name, email FROM user" ``` 特定のカラムの数を取得したい場合は、指定したカラムで`count`を使用することで構築できます。  ```scala -val select = Table[Task] - .select(_.id.count) +val select = Table[User].select(_.id.count) -select.statement === "SELECT COUNT(id) FROM task" +select.statement === "SELECT COUNT(id) FROM user" ``` ### WHERE @@ -108,36 +70,35 @@ select.statement === "SELECT COUNT(id) FROM task" クエリに型安全にWhere条件を設定する方法は`where`メソッドを使用することです。 ```scala -val where = Table[Task] - .where(_.done === true) +val where = Table[User].selectAll.where(_.email === "alice@example.com") -where.statement === "SELECT id, name, done FROM task WHERE done = ?" +where.statement === "SELECT id, name, email FROM user WHERE email = ?" ``` `where`メソッドで使用できる条件の一覧は以下です。 -| 条件 | ステートメント | -|--------------------------------------|---------------------------------------| -| === | `column = ?` | -| >= | `column >= ?` | -| > | `column > ?` | -| <= | `column <= ?` | -| < | `column < ?` | -| <> | `column <> ?` | -| !== | `column != ?` | -| IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL") | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | -| <=> | `column <=> ?` | -| IN (value, value, ...) | `column IN (?, ?, ...)` | -| BETWEEN (start, end) | `column BETWEEN ? AND ?` | -| LIKE (value) | `column LIKE ?` | -| LIKE_ESCAPE (like, escape) | `column LIKE ? ESCAPE ?` | -| REGEXP (value) | `column REGEXP ?` | -| `<<` (value) | `column << ?` | -| `>>` (value) | `column >> ?` | -| DIV (cond, result) | `column DIV ? = ?` | -| MOD (cond, result) | `column MOD ? = ?` | -| ^ (value) | `column ^ ?` | -| ~ (value) | `~column = ?` | +| 条件 | ステートメント | +|----------------------------------------|---------------------------------------| +| `===` | `column = ?` | +| `>=` | `column >= ?` | +| `>` | `column > ?` | +| `<=` | `column <= ?` | +| `<` | `column < ?` | +| `<>` | `column <> ?` | +| `!==` | `column != ?` | +| `IS ("TRUE"/"FALSE"/"UNKNOWN"/"NULL")` | `column IS {TRUE/FALSE/UNKNOWN/NULL}` | +| `<=>` | `column <=> ?` | +| `IN (value, value, ...)` | `column IN (?, ?, ...)` | +| `BETWEEN (start, end)` | `column BETWEEN ? AND ?` | +| `LIKE (value)` | `column LIKE ?` | +| `LIKE_ESCAPE (like, escape)` | `column LIKE ? ESCAPE ?` | +| `REGEXP (value)` | `column REGEXP ?` | +| `<<` (value) | `column << ?` | +| `>>` (value) | `column >> ?` | +| `DIV (cond, result)` | `column DIV ? = ?` | +| `MOD (cond, result)` | `column MOD ? = ?` | +| `^ (value)` | `column ^ ?` | +| `~ (value)` | `~column = ?` | ### GROUP BY/Having @@ -146,11 +107,11 @@ where.statement === "SELECT id, name, done FROM task WHERE done = ?" `groupBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準にグループ化することができます。 ```scala -val select = Table[Task] - .select(task => (task.id, task.name)) +val select = Table[User] + .select(user => (user.id, user.name)) .groupBy(_._2) -select.statement === "SELECT id, name FROM task GROUP BY name" +select.statement === "SELECT id, name FROM user GROUP BY name" ``` グループ化すると`select`で取得できるデータの数はグループの数だけとなります。そこでグループ化を行った場合には、グループ化に指定したカラムの値や、用意された関数を使ってカラムの値をグループ単位で集計した結果などを取得することができます。 @@ -158,12 +119,12 @@ select.statement === "SELECT id, name FROM task GROUP BY name" `having`を使用すると`groupBy`によってグループ化されて取得したデータに関して、取得する条件を設定することができます。 ```scala -val select = Table[Task] - .select(task => (task.id, task.name)) +val select = Table[User] + .select(user => (user.id, user.name)) .groupBy(_._2) .having(_._1 > 1) -select.statement === "SELECT id, name FROM task GROUP BY name HAVING id > ?" +select.statement === "SELECT id, name FROM user GROUP BY name HAVING id > ?" ``` ### ORDER BY @@ -173,21 +134,21 @@ select.statement === "SELECT id, name FROM task GROUP BY name HAVING id > ?" `orderBy`を使用することで`select`でデータを取得する時に指定したカラム名の値を基準に昇順、降順で並び替えることができます。 ```scala -val select = Table[Task] - .select(task => (task.id, task.name)) +val select = Table[User] + .select(user => (user.id, user.name)) .orderBy(_.id) -select.statement === "SELECT id, name FROM task ORDER BY id" +select.statement === "SELECT id, name FROM user ORDER BY id" ``` 昇順/降順を指定したい場合は、それぞれカラムに対して `asc`/`desc`を呼び出すだけです。 ```scala -val select = Table[Task] - .select(task => (task.id, task.name)) +val select = Table[User] + .select(user => (user.id, user.name)) .orderBy(_.id.asc) -select.statement === "SELECT id, name FROM task ORDER BY id ASC" +select.statement === "SELECT id, name FROM user ORDER BY id ASC" ``` ### LIMIT/OFFSET @@ -197,12 +158,12 @@ select.statement === "SELECT id, name FROM task ORDER BY id ASC" `limit`を設定すると`select`を実行した時に取得するデータの行数の上限を設定することができ、`offset`を設定すると何番目からのデータを取得するのかを指定することができます。 ```scala -val select = Table[Task] - .select(task => (task.id, task.name)) +val select = Table[User] + .select(user => (user.id, user.name)) .limit(1) .offset(1) -select.statement === "SELECT id, name FROM task LIMIT ? OFFSET ?" +select.statement === "SELECT id, name FROM user LIMIT ? OFFSET ?" ``` ## JOIN/LEFT JOIN/RIGHT JOIN @@ -211,14 +172,20 @@ select.statement === "SELECT id, name FROM task LIMIT ? OFFSET ?" Joinでは以下定義をサンプルとして使用します。 -```scala -case class Country(code: String, name: String) derives Table -case class City(id: Int, name: String, countryCode: String) derives Table -case class CountryLanguage(countryCode: String, language: String) derives Table +```scala 3 +case class User(id: Int, name: String, email: String) derives Table +case class Product(id: Int, name: String, price: BigDecimal) derives Table +case class Order( + id: Int, + userId: Int, + productId: Int, + orderDate: LocalDateTime, + quantity: Int +) derives Table -val countryTable = Table[Country] -val cityTable = Table[City] -val countryLanguageTable = Table[CountryLanguage] +val userTable = Table[User] +val productTable = Table[Product] +val orderTable = Table[Order] ``` まずシンプルなJoinを行いたい場合は、`join`を使用します。 @@ -227,20 +194,20 @@ val countryLanguageTable = Table[CountryLanguage] Join後の`select`は2つのテーブルからカラムを指定することになります。 ```scala -val join = countryTable.join(cityTable)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) +val join = userTable.join(orderTable)((user, order) => user.id === order.userId) + .select((user, order) => (user.name, order.quantity)) -join.statement = "SELECT country.`name`, city.`name` FROM country JOIN city ON country.code = city.country_code" +join.statement = "SELECT user.`name`, order.`quantity` FROM user JOIN order ON user.id = order.user_id" ``` 次に左外部結合であるLeft Joinを行いたい場合は、`leftJoin`を使用します。 `join`が`leftJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 ```scala 3 -val leftJoin = countryTable.leftJoin(cityTable)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) +val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) + .select((user, order) => (user.name, order.quantity)) -join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city ON country.code = city.country_code" +join.statement = "SELECT user.`name`, order.`quantity` FROM user LEFT JOIN order ON user.id = order.user_id" ``` シンプルなJoinとの違いは`leftJoin`を使用した場合、結合を行うテーブルから取得するレコードはNULLになる可能性があるということです。 @@ -248,18 +215,18 @@ join.statement = "SELECT country.`name`, city.`name` FROM country LEFT JOIN city そのためldbcでは`leftJoin`に渡されたテーブルから取得するカラムのレコードは全てOption型になります。 ```scala 3 -val leftJoin = countryTable.leftJoin(cityTable)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (String, Option[String]) +val leftJoin = userTable.leftJoin(orderTable)((user, order) => user.id === order.userId) + .select((user, order) => (user.name, order.quantity)) // (String, Option[Int]) ``` 次に右外部結合であるRight Joinを行いたい場合は、`rightJoin`を使用します。 こちらも`join`が`rightJoin`に変わっただけで実装自体はシンプルなJoinの時と同じになります。 ```scala 3 -val rightJoin = countryTable.rightJoin(cityTable)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) +val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) + .select((order, user) => (order.quantity, user.name)) -join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN city ON country.code = city.country_code" +join.statement = "SELECT order.`quantity`, user.`name` FROM order RIGHT JOIN user ON order.user_id = user.id" ``` シンプルなJoinとの違いは`rightJoin`を使用した場合、結合元のテーブルから取得するレコードはNULLになる可能性があるということです。 @@ -267,27 +234,27 @@ join.statement = "SELECT country.`name`, city.`name` FROM country RIGHT JOIN cit そのためldbcでは`rightJoin`を使用した結合元のテーブルから取得するカラムのレコードは全てOption型になります。 ```scala 3 -val rightJoin = countryTable.rightJoin(cityTable)((country, city) => country.code === city.countryCode) - .select((country, city) => (country.name, city.name)) // (Option[String], String) +val rightJoin = orderTable.rightJoin(userTable)((order, user) => order.userId === user.id) + .select((order, user) => (order.quantity, user.name)) // (Option[Int], String) ``` 複数のJoinを行いたい場合は、メソッドチェーンで任意のJoinメソッドを呼ぶことで実現することができます。 ```scala 3 val join = - (countryTable join cityTable)((country, city) => country.code === city.countryCode) - .rightJoin(countryLanguageTable)((_, city, countryLanguage) => city.countryCode === countryLanguage.countryCode) - .select((country, city, countryLanguage) => (country.name, city.name, countryLanguage.language)) // (Option[String], Option[String], String)] + (productTable join orderTable)((product, order) => product.id === order.productId) + .rightJoin(userTable)((_, order, user) => order.userId === user.id) + .select((product, order, user) => (product.name, order.quantity, user.name)) // (Option[String], Option[Int], String)] join.statement = """ |SELECT - | country.`name`, - | city.`name`, - | country_language.`language` - |FROM country - |JOIN city ON country.code = city.country_code - |RIGHT JOIN country_language ON city.country_code = country_language.country_code + | product.`name`, + | order.`quantity`, + | user.`name` + |FROM product + |JOIN order ON product.id = order.product_id + |RIGHT JOIN user ON order.user_id = user.id |""".stripMargin ``` @@ -307,17 +274,17 @@ join.statement = `insert`メソッドには挿入するデータのタプルを渡します。タプルはモデルと同じプロパティの数と型である必要があります。また、挿入されるデータの順番はモデルのプロパティおよびテーブルのカラムと同じ順番である必要があります。 ```scala 3 -val insert = task.insert((1L, "name", false)) +val insert = user.insert((1, "name", "email@example.com")) -insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?)" +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" ``` 複数のデータを挿入したい場合は、`insert`メソッドに複数のタプルを渡すことで構築できます。 ```scala 3 -val insert = task.insert((1L, "name", false), (2L, "name", true)) +val insert = user.insert((1, "name 1", "email+1@example.com"), (2, "name 2", "email+2@example.com")) -insert.statement === "INSERT INTO task (`id`, `name`, `age`) VALUES(?, ?, ?), (?, ?, ?)" +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" ``` **insertInto** @@ -327,17 +294,17 @@ insert.statement === "INSERT INTO task (`id`, `name`, `age`) VALUES(?, ?, ?), (? これはAutoIncrementやDefault値を持つカラムへのデータ挿入を除外したい場合などに使用できます。 ```scala 3 -val insert = task.insertInto(task => (task.name, task.done)).values(("name", false)) +val insert = user.insertInto(user => (user.name, user.email)).values(("name 3", "email+3@example.com")) -insert.statement === "INSERT INTO task (`name`, `done`) VALUES(?, ?)" +insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?)" ``` 複数のデータを挿入したい場合は、`values`にタプルの配列を渡すことで構築できます。 ```scala 3 -val insert = task.insertInto(task => (task.name, task.done)).values(List(("name", false), ("name", true))) +val insert = user.insertInto(user => (user.name, user.email)).values(List(("name 4", "email+4@example.com"), ("name 5", "email+5@example.com"))) -insert.statement === "INSERT INTO task (`name`, `done`) VALUES(?, ?), (?, ?)" +insert.statement === "INSERT INTO user (`name`, `email`) VALUES(?, ?), (?, ?)" ``` **+=** @@ -345,9 +312,9 @@ insert.statement === "INSERT INTO task (`name`, `done`) VALUES(?, ?), (?, ?)" `+=`メソッドを使用することでモデルを使用してinsert文を構築することができます。モデルを使用する場合は全てのカラムにデータを挿入してしまうことに注意してください。 ```scala 3 -val insert = task += Task(1L, "name", false) +val insert = user += User(6, "name 6", "email+6@example.com") -insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?)" +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?)" ``` **++=** @@ -355,9 +322,9 @@ insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?)" モデルを使用して複数のデータを挿入したい場合は`++=`メソッドを使用します。 ```scala 3 -val insert = task ++= List(Task(1L, "name", false), Task(2L, "name", true)) +val insert = user ++= List(User(7, "name 7", "email+7@example.com"), User(8, "name 8", "email+8@example.com")) -insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?), (?, ?, ?)" +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?), (?, ?, ?)" ``` ### ON DUPLICATE KEY UPDATE @@ -367,9 +334,9 @@ ON DUPLICATE KEY UPDATE 句を指定し行を挿入すると、UNIQUEインデ ldbcでこの処理を実現する方法は、`Insert`に対して`onDuplicateKeyUpdate`を使用することです。 ```scala -val insert = task.insert((1L, "name", false)).onDuplicateKeyUpdate(v => (v.name, v.done)) +val insert = user.insert((9, "name", "email+9@example.com")).onDuplicateKeyUpdate(v => (v.name, v.email)) -insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?) AS new_task ON DUPLICATE KEY UPDATE `name` = new_task.`name`, `done` = new_task.`done`" +insert.statement === "INSERT INTO user (`id`, `name`, `email`) VALUES(?, ?, ?) AS new_user ON DUPLICATE KEY UPDATE `name` = new_user.`name`, `email` = new_user.`email`" ``` ## UPDATE @@ -379,39 +346,39 @@ insert.statement === "INSERT INTO task (`id`, `name`, `done`) VALUES(?, ?, ?) AS `update`メソッドの第1引数にはテーブルのカラム名ではなくモデルのプロパティ名を指定し、第2引数に更新したい値を渡します。第2引数に渡す値の型は第1引数で指定したプロパティの型と同じである必要があります。 ```scala -val update = task.update("name", "update name") +val update = user.update("name", "update name") -update.statement === "UPDATE task SET name = ?" +update.statement === "UPDATE user SET name = ?" ``` 第1引数に存在しないプロパティ名を指定した場合コンパイルエラーとなります。 ```scala 3 -val update = task.update("hoge", "update name") // Compile error +val update = user.update("hoge", "update name") // Compile error ``` 複数のカラムを更新したい場合は`set`メソッドを使用します。 ```scala 3 -val update = task.update("name", "update name").set("done", false) +val update = user.update("name", "update name").set("email", "update-email@example.com") -update.statement === "UPDATE task SET name = ?, done = ?" +update.statement === "UPDATE user SET name = ?, email = ?" ``` `set`メソッドには条件に応じてクエリを生成させないようにすることもできます。 ```scala 3 -val update = task.update("name", "update name").set("done", false, false) +val update = user.update("name", "update name").set("email", "update-email@example.com", false) -update.statement === "UPDATE task SET name = ?" +update.statement === "UPDATE user SET name = ?" ``` モデルを使用してupdate文を構築することもできます。モデルを使用する場合は全てのカラムを更新してしまうことに注意してください。 ```scala 3 -val update = task.update(Task(1L, "update name", false)) +val update = user.update(User(1, "update name", "update-email@example.com")) -update.statement === "UPDATE task SET id = ?, name = ?, done = ?" +update.statement === "UPDATE user SET id = ?, name = ?, email = ?" ``` ## DELETE @@ -419,7 +386,7 @@ update.statement === "UPDATE task SET id = ?, name = ?, done = ?" 型安全にDELETE文を構築する方法はTableが提供する`delete`メソッドを使用することです。 ```scala -val delete = task.delete +val delete = user.delete -delete.statement === "DELETE FROM task" +delete.statement === "DELETE FROM user" ``` diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 32ac31baf..89238dbbc 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -10,5 +10,6 @@ laika.navigationOrder = [ Updating-Data.md, Error-Handling.md, Logging.md, - Custom-Data-Type.md + Custom-Data-Type.md, + Query-Builder.md ] From 4e450ec1add598fa6adbfb28bcd8bab1019c298e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 18:05:29 +0900 Subject: [PATCH 088/160] Create Schema tutorial document --- .../main/mdoc/ja/tutorial/Schema.md} | 262 +++++++++--------- docs/src/main/mdoc/ja/tutorial/directory.conf | 3 +- 2 files changed, 137 insertions(+), 128 deletions(-) rename docs/{old/11-Schema.md => src/main/mdoc/ja/tutorial/Schema.md} (63%) diff --git a/docs/old/11-Schema.md b/docs/src/main/mdoc/ja/tutorial/Schema.md similarity index 63% rename from docs/old/11-Schema.md rename to docs/src/main/mdoc/ja/tutorial/Schema.md index b49b3b2ea..aa1beea69 100644 --- a/docs/old/11-Schema.md +++ b/docs/src/main/mdoc/ja/tutorial/Schema.md @@ -1,6 +1,17 @@ +{% + laika.title = スキーマ + laika.metadata.language = ja +%} + # スキーマ -この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、[code generator](/ldbc/ja/07-Schema-Code-Generation.html) を使ってこの作業を省略することもできます。 +この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、Code Generatorを使ってこの作業を省略することもできます。 + +プロジェクトに以下の依存関係を設定する必要があります。 + +```scala +//> using dep "@ORGANIZATION@::ldbc-schema:@VERSION@" +``` 以下のコード例では、以下のimportを想定しています。 @@ -15,16 +26,16 @@ ldbc は、このテーブル定義をさまざまな目的で使用します。 ```scala 3 case class User( - id: Long, - name: String, - age: Option[Int], + id: Int, + name: String, + email: String, ) -val table = Table[User]("user")( // CREATE TABLE `user` ( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), // `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - column("name", VARCHAR(255)), // `name` VARCHAR(255) NOT NULL, - column("age", INT.UNSIGNED.DEFAULT(None)), // `age` INT unsigned DEFAULT NULL -) // ); +val table = Table[User]("user")( // CREATE TABLE `user` ( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), // `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + column("name", VARCHAR(50)), // `name` VARCHAR(50) NOT NULL, + column("email", VARCHAR(100)), // `email` VARCHAR(100) NOT NULL, +) // ); ``` すべてのカラムはcolumnメソッドで定義されます。各カラムにはカラム名、データ型、属性があります。以下のプリミティブ型が標準でサポートされており、すぐに使用できます。 @@ -36,7 +47,7 @@ val table = Table[User]("user")( // CREATE TABLE `user` ( - Boolean - java.time.* -Null可能な列はOption[T]で表現され、Tはサポートされるプリミティブ型の1つです。Option型でない列はすべてNot Nullであることに注意してください。 +Null可能な列は`Option[T]`で表現され、Tはサポートされるプリミティブ型の1つです。Option型でない列はすべてNot Nullであることに注意してください。 ## データ型 @@ -44,48 +55,48 @@ Null可能な列はOption[T]で表現され、Tはサポートされるプリミ データ型がサポートするScalaの型は以下の表の通りです。 -| Data Type | Scala Type | -|------------|-----------------------------------------------------------------------------------------------| -| BIT | Byte, Short, Int, Long | -| TINYINT | Byte, Short | -| SMALLINT | Short, Int | -| MEDIUMINT | Int | -| INT | Int, Long | -| BIGINT | Long, BigInt | -| DECIMAL | BigDecimal | -| FLOAT | Float | -| DOUBLE | Double | -| CHAR | String | -| VARCHAR | String | -| BINARY | Array[Byte] | -| VARBINARY | Array[Byte] | -| TINYBLOB | Array[Byte] | -| BLOB | Array[Byte] | -| MEDIUMBLOB | Array[Byte] | -| LONGBLOB | Array[Byte] | -| TINYTEXT | String | -| TEXT | String | -| MEDIUMTEXT | String | -| DATE | java.time.LocalDate | -| DATETIME | java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime | -| TIMESTAMP | java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime | -| TIME | java.time.LocalTime | -| YEAR | java.time.Instant, java.time.LocalDate, java.time.Year | -| BOOLEAN | Boolean | +| Data Type | Scala Type | +|--------------|-------------------------------------------------------------------------------------------------| +| `BIT` | `Byte, Short, Int, Long` | +| `TINYINT` | `Byte, Short` | +| `SMALLINT` | `Short, Int` | +| `MEDIUMINT` | `Int` | +| `INT` | `Int, Long` | +| `BIGINT` | `Long, BigInt` | +| `DECIMAL` | `BigDecimal` | +| `FLOAT` | `Float` | +| `DOUBLE` | `Double` | +| `CHAR` | `String` | +| `VARCHAR` | `String` | +| `BINARY` | `Array[Byte]` | +| `VARBINARY` | `Array[Byte]` | +| `TINYBLOB` | `Array[Byte]` | +| `BLOB` | `Array[Byte]` | +| `MEDIUMBLOB` | `Array[Byte]` | +| `LONGBLOB` | `Array[Byte]` | +| `TINYTEXT` | `String` | +| `TEXT` | `String` | +| `MEDIUMTEXT` | `String` | +| `DATE` | `java.time.LocalDate` | +| `DATETIME` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetTime` | +| `TIMESTAMP` | `java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime, java.time.ZonedDateTime` | +| `TIME` | `java.time.LocalTime` | +| `YEAR` | `java.time.Instant, java.time.LocalDate, java.time.Year` | +| `BOOLEA` | `Boolean` | **整数型を扱う際の注意点** 符号あり、符号なしに応じて、扱えるデータの範囲がScalaの型に収まらないことに注意する必要があります。 -| Data Type | signed range | unsigned range | Scala Type | range | -|-----------|--------------------------------------------|--------------------------|----------------|--------------------------------------------------------------------| -| TINYINT | -128 ~ 127 | 0 ~ 255 | Byte
Short | -128 ~ 127
-32768~32767 | -| SMALLINT | -32768 ~ 32767 | 0 ~ 65535 | Short
Int | -32768~32767
-2147483648~2147483647 | -| MEDIUMINT | -8388608 ~ 8388607 | 0 ~ 16777215 | Int | -2147483648~2147483647 | -| INT | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | Int
Long | -2147483648~2147483647
-9223372036854775808~9223372036854775807 | -| BIGINT | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 18446744073709551615 | Long
BigInt | -9223372036854775808~9223372036854775807
... | +| Data Type | Signed Range | Unsigned Range | Scala Type | Range | +|-------------|----------------------------------------------|----------------------------|------------------|----------------------------------------------------------------------| +| `TINYINT` | `-128 ~ 127` | `0 ~ 255` | `Byte
Short` | `-128 ~ 127
-32768~32767` | +| `SMALLINT` | `-32768 ~ 32767` | `0 ~ 65535` | `Short
Int` | `-32768~32767
-2147483648~2147483647` | +| `MEDIUMINT` | `-8388608 ~ 8388607` | `0 ~ 16777215` | `Int` | `-2147483648~2147483647` | +| `INT` | `-2147483648 ~ 2147483647` | `0 ~ 4294967295` | `Int
Long` | `-2147483648~2147483647
-9223372036854775808~9223372036854775807` | +| `BIGINT` | `-9223372036854775808 ~ 9223372036854775807` | `0 ~ 18446744073709551615` | `Long
BigInt` | `-9223372036854775808~9223372036854775807
...` | -ユーザー定義の独自型やサポートされていない型を扱う場合は、[カスタム型](/ldbc/ja/02-Custom-Data-Type.html) を参照してください。 +ユーザー定義の独自型やサポートされていない型を扱う場合は、カスタムデータ型を参照してください。 ## 属性 @@ -128,9 +139,9 @@ ldbcのテーブル定義には `keySet`というメソッドが生えており ```scala 3 val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) .keySet(table => PRIMARY_KEY(table.id)) @@ -151,9 +162,9 @@ val table = Table[User]("user")( ```scala 3 val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) .keySet(table => PRIMARY_KEY(table.id, table.name)) @@ -169,9 +180,9 @@ ldbcではテーブル定義に複数`PRIMARY_KEY`を設定したとしてもコ ```scala 3 val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255), PRIMARY_KEY), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50), PRIMARY_KEY), + column("email", VARCHAR(100)) ) // CREATE TABLE `user` ( @@ -202,9 +213,9 @@ ldbcのテーブル定義には `keySet`というメソッドが生えており ```scala 3 val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) .keySet(table => UNIQUE_KEY(table.id)) @@ -226,9 +237,9 @@ val table = Table[User]("user")( ```scala 3 val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) .keySet(table => UNIQUE_KEY(table.id, table.name)) @@ -263,9 +274,9 @@ ldbcのテーブル定義には `keySet`というメソッドが生えており ```scala 3 val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) .keySet(table => INDEX_KEY(table.id)) @@ -287,9 +298,9 @@ val table = Table[User]("user")( ```scala 3 val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) .keySet(table => INDEX_KEY(table.id, table.name)) @@ -308,22 +319,22 @@ val table = Table[User]("user")( ldbcではこの外部キー制約をtableのkeySetメソッドを使用する方法で設定することができます。 ```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)) +val user = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + ... ) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id))) + .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id))) -// CREATE TABLE `user` ( +// CREATE TABLE `order` ( // ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// FOREIGN KEY (user_id) REFERENCES `user` (id), // ) ``` @@ -336,17 +347,16 @@ val user = Table[User]("user")( 設定することのできる値は`ldbc.schema.Reference.ReferenceOption`から取得することができます。 ```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + ... ) - .keySet(table => FOREIGN_KEY(table.postId, REFERENCE(post, post.id).onDelete(Reference.ReferenceOption.RESTRICT))) + .keySet(table => FOREIGN_KEY(table.userId, REFERENCE(user, user.id).onDelete(Reference.ReferenceOption.RESTRICT))) -// CREATE TABLE `user` ( +// CREATE TABLE `order` ( // ..., -// FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT +// FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT // ) ``` @@ -363,24 +373,23 @@ val user = Table[User]("user")( 1つのカラムだけではなく、複数のカラムを外部キーとして組み合わせて設定することもできます。`FOREIGN_KEY`に外部キーとして設定したいカラムを複数渡すだけで複合外部キーとして設定することができます。 ```scala 3 -val post = Table[Post]("post")( - column("id", BIGINT[Long], AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("category", SMALLINT[Short]) +val user = Table[User]("user")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]), - column("post_category", SMALLINT[Short]) +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + column("user_email", VARCHAR(100)) + ... ) - .keySet(table => FOREIGN_KEY((table.postId, table.postCategory), REFERENCE(post, (post.id, post.category)))) + .keySet(table => FOREIGN_KEY((table.userId, table.userEmail), REFERENCE(user, (user.id, user.email)))) // CREATE TABLE `user` ( // ..., -// FOREIGN KEY (`post_id`, `post_category`) REFERENCES `post` (`id`, `category`) +// FOREIGN KEY (`user_id`, `user_email`) REFERENCES `user` (`id`, `email`) // ) ``` @@ -391,17 +400,16 @@ MySQLではCONSTRAINTを使用することで制約に対して任意の名前 ldbcではCONSTRAINTメソッドが提供されているのでキー制約などの制約を設定する処理をCONSTRAINTメソッドに渡すだけで設定することができます。 ```scala 3 -val user = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), - column("post_id", BIGINT[Long]) +val order = Table[Order]("order")( + column("id", INT, AUTO_INCREMENT, PRIMARY_KEY), + column("user_id", VARCHAR(50)) + ... ) - .keySet(table => CONSTRAINT("fk_post_id", FOREIGN_KEY(table.postId, REFERENCE(post, post.id)))) + .keySet(table => CONSTRAINT("fk_user_id", FOREIGN_KEY(table.userId, REFERENCE(user, user.id)))) -// CREATE TABLE `user` ( +// CREATE TABLE `order` ( // ..., -// CONSTRAINT `fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) +// CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) // ) ``` @@ -411,9 +419,9 @@ val user = Table[User]("user")( ```scala 3 case class User( - id: Long, - name: User.Name, - age: Option[Int], + id: Int, + name: User.Name, + email: String, ) object User: @@ -423,21 +431,21 @@ object User: given Conversion[VARCHAR[String], DataType[Name]] = DataType.mapping[VARCHAR[String], Name] val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT), + column("name", VARCHAR(50)), + column("email", VARCHAR(100)) ) ``` -LDBCでは複数のカラムをモデルが持つ1つのプロパティに統合することはできません。LDBCの目的はモデルとテーブルを1対1でマッピングを行い、データベースのテーブル定義を型安全に構築することにあるからです。 +ldbcでは複数のカラムをモデルが持つ1つのプロパティに統合することはできません。ldbcの目的はモデルとテーブルを1対1でマッピングを行い、データベースのテーブル定義を型安全に構築することにあるからです。 そのためテーブル定義とモデルで異なった数のプロパティを持つようなことは許可していません。以下のような実装はコンパイルエラーとなります。 ```scala 3 case class User( - id: Long, - name: User.Name, - age: Option[Int], + id: Int, + name: User.Name, + email: String, ) object User: @@ -445,10 +453,10 @@ object User: case class Name(firstName: String, lastName: String) val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT), + column("first_name", VARCHAR(50)), + column("last_name", VARCHAR(50)), + column("email", VARCHAR(100)) ) ``` @@ -456,10 +464,10 @@ object User: ```scala 3 case class User( - id: Long, + id: Int, firstName: String, - lastName: String, - age: Option[Int], + lastName: String, + email: String, ): val name: User.Name = User.Name(firstName, lastName) @@ -469,9 +477,9 @@ object User: case class Name(firstName: String, lastName: String) val table = Table[User]("user")( - column("id", BIGINT[Long], AUTO_INCREMENT), - column("first_name", VARCHAR(255)), - column("last_name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)) + column("id", INT, AUTO_INCREMENT), + column("first_name", VARCHAR(50)), + column("last_name", VARCHAR(50)), + column("email", VARCHAR(100)) ) ``` diff --git a/docs/src/main/mdoc/ja/tutorial/directory.conf b/docs/src/main/mdoc/ja/tutorial/directory.conf index 89238dbbc..81cb1cb1c 100644 --- a/docs/src/main/mdoc/ja/tutorial/directory.conf +++ b/docs/src/main/mdoc/ja/tutorial/directory.conf @@ -11,5 +11,6 @@ laika.navigationOrder = [ Error-Handling.md, Logging.md, Custom-Data-Type.md, - Query-Builder.md + Query-Builder.md, + Schema.md ] From 110ac97340c5caad10ceeb0dde577c772cd744cb Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 18:06:14 +0900 Subject: [PATCH 089/160] Delete unused --- docs/old/03-Connecting-to-a-Database.md | 176 ------------------------ 1 file changed, 176 deletions(-) delete mode 100644 docs/old/03-Connecting-to-a-Database.md diff --git a/docs/old/03-Connecting-to-a-Database.md b/docs/old/03-Connecting-to-a-Database.md deleted file mode 100644 index dd64ef5a1..000000000 --- a/docs/old/03-Connecting-to-a-Database.md +++ /dev/null @@ -1,176 +0,0 @@ -# データベースへの接続 - -この章では最初から始めます。まず、データベースに接続して値を返すプログラムを書き、そのプログラムをREPLで実行する。また、小さなプログラムを組み合わせてより大きなプログラムを構築することにも触れます。 - -## セットアップ - -まず、データベースを起動します。以下のコードを使用して、データベースを起動します。 - -```yaml -version: '3' -services: - mysql: - image: mysql@MYSQL_VERSION@ - container_name: ldbc - environment: - MYSQL_USER: 'ldbc' - MYSQL_PASSWORD: 'password' - MYSQL_ROOT_PASSWORD: 'root' - ports: - - 13306:3306 - volumes: - - ./database:/docker-entrypoint-initdb.d - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 -``` - -次に、データベースの初期化を行います。 - -以下コードのようにデータベースの作成を行います。 - -@:include(/docs/src/main/scala/00-Setup.scala) { #setupDatabase } - -次に、テーブルの作成を行います。 - -ここでは「ユーザー(user)」、「注文(order)」、「製品(product)」の3つのテーブルを使用した各ユーザーが複数の注文を行い、各注文が特定の製品に関連付けられている状況をシミュレートします。 - -**ユーザー(user)** - -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupUser } - -**注文(order)** - -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupProduct } - -**製品(product)** - -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupOrder } - -それぞれのテーブルにデータを挿入します。 - -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertUser } -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertProduct } -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertOrder } - -そしてデータベースに接続するためのコネクタを作成する。コネクタは、データベースへの接続を管理するためのリソースである。コネクタは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 - -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #connection } - -最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 - -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #setupTable } -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #insertData } -@@snip [00-Setup.scala](/docs/src/main/scala/00-Setup.scala) { #run } - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## 最初のプログラム - -ldbcを使う前に、いくつかのシンボルをインポートする必要がある。ここでは便宜上、パッケージのインポートを使用する。これにより、高レベルAPIで作業する際に最もよく使用されるシンボルを得ることができる。 - -```scala -import ldbc.dsl.io.* -``` - -Catsも連れてこよう。 - -```scala -import cats.syntax.all.* -import cats.effect.* -``` - -次に、トレーサーとログハンドラーを提供する。これらは、アプリケーションのログを記録するために使用される。トレーサーは、アプリケーションのトレースを記録するために使用される。ログハンドラーは、アプリケーションのログを記録するために使用される。 - -以下のコードは、トレーサーとログハンドラーを提供するがその実体は何もしない。 - -@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #given } - -ldbc高レベルAPIで扱う最も一般的な型はExecutor[F, A]という形式で、{java | ldbc}.sql.Connectionが利用可能なコンテキストで行われる計算を指定し、最終的にA型の値を生成します。 - -では、定数を返すだけのExecutorプログラムから始めてみよう。 - -@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #program } - -次に、データベースに接続するためのコネクタを作成する。コネクタは、データベースへの接続を管理するためのリソースである。コネクタは、データベースへの接続を開始し、クエリを実行し、接続を閉じるためのリソースを提供する。 - -@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #connection } - -Executorは、データベースへの接続方法、接続の受け渡し方法、接続のクリーンアップ方法を知っているデータ型であり、この知識によってExecutorをIOへ変換し、実行可能なプログラムを得ることができる。具体的には、実行するとデータベースに接続し、単一のトランザクションを実行するIOが得られる。 - -@@snip [01-Program.scala](/docs/src/main/scala/01-Program.scala) { #run } - -万歳!定数を計算できた。これはデータベースに仕事を依頼することはないので、あまり面白いものではないが、最初の一歩が完了です。 - -> Keep in mind that all the code in this book is pure except the calls to IO.unsafeRunSync, which is the “end of the world” operation that typically appears only at your application’s entry points. In the REPL we use it to force a computation to “happen”. - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/01-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## 2つめのプログラム - -では、sql string interpolatorを使って、データベースに定数の計算を依頼する問い合わせを作成してみましょう。 - -@@snip [02-Program.scala](/docs/src/main/scala/02-Program.scala) { #program } - -最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 - -@@snip [02-Program.scala](/docs/src/main/scala/02-Program.scala) { #run } - -定数を計算するためにデータベースに接続した。かなり印象的だ。 - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/02-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## 3つめのプログラム - -一つの取引で複数のことをしたい場合はどうすればいいのか?簡単だ!Executorはモナドなので、for内包を使って2つの小さなプログラムを1つの大きなプログラムにすることができる。 - -@@snip [03-Program.scala](/docs/src/main/scala/03-Program.scala) { #program } - -最後に、データベースに接続して値を返すプログラムを書く。このプログラムは、データベースに接続し、クエリを実行し、結果を取得する。 - -@@snip [03-Program.scala](/docs/src/main/scala/03-Program.scala) { #run } - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/03-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` - -## 4つめのプログラム - -データベースに対して書き込みを行うプログラムを書いてみよう。ここでは、データベースに接続し、クエリを実行し、データを挿入する。 - -@@snip [04-Program.scala](/docs/src/main/scala/04-Program.scala) { #program } - -先ほどと異なる点は、`commit`メソッドを呼び出すことである。これにより、トランザクションがコミットされ、データベースにデータが挿入される。 - -@@snip [04-Program.scala](/docs/src/main/scala/04-Program.scala) { #run } - -**Scala CLIで実行** - -このプログラムは、Scala CLIを使って実行することができる。以下のコマンドを実行すると、このプログラムを実行することができる。 - -```shell -scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/04-Program.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ -``` From 32f109248cf8966beb867cd755d2c498eb3e84ac Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 18:06:28 +0900 Subject: [PATCH 090/160] Fixed Migration-Notes.md --- docs/src/main/mdoc/Migration-Notes.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/src/main/mdoc/Migration-Notes.md b/docs/src/main/mdoc/Migration-Notes.md index 309494e6a..81041c51f 100644 --- a/docs/src/main/mdoc/Migration-Notes.md +++ b/docs/src/main/mdoc/Migration-Notes.md @@ -1,21 +1,24 @@ {% laika.title = Migration Notes - laika.metadata.language = en + laika.metadata { + language = en + isRootPath = true + } %} -# 移行ノート +# Migration Notes ## Upgrading to 0.3.x from 0.2.x ### Packages -**パッケージ名の変更** +**Change package name** | 0.2.x | 0.3.x | |-----------|-------------| | ldbc-core | ldbc-schema | -**新規パッケージ** +**New packages** 新たに2種類のパッケージが追加されました。 From ec7ec9636b2f4d9961753d9e7d1adf7f4779f432 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 18:06:47 +0900 Subject: [PATCH 091/160] Delete text --- docs/src/main/mdoc/en/index.md | 112 --------------------------------- 1 file changed, 112 deletions(-) diff --git a/docs/src/main/mdoc/en/index.md b/docs/src/main/mdoc/en/index.md index a35e80ce8..25a5207dc 100644 --- a/docs/src/main/mdoc/en/index.md +++ b/docs/src/main/mdoc/en/index.md @@ -6,115 +6,3 @@ # LDBC Note that **LDBC** is pre-1.0 software and is still under active development. Newer versions may no longer be binary compatible with earlier versions. - -## Introduction - -Most of our application development involves the use of databases.
One way to access databases in Scala is to use JDBC, and there are several libraries in Scala that wrap this JDBC. - -- Functional DSL (slick, quill, zio-sql) -- SQL string interpolator (Anorm, doobie) - -LDBC, also a JDBC-wrapped library, is a Scala 3 library that combines aspects of each, providing a type-safe, refactorable SQL interface that can express SQL expressions on a MySQL database. - -The concept of LDBC also allows development to centralize Scala models, sql schemas, and documents by using LDBC to manage a single resource. - -This concept was influenced by [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library.
By using tapir, you can build type-safe endpoints and even generate OpenAPI documents from the endpoints you build. - -LDBC uses Scala at the database layer to allow for the same type-safe construction and to allow documentation generation using what has been constructed. - -## Why LDBC? - -Development of database-based applications requires a variety of ongoing changes. - -For example, what information in the tables built in the database should be handled by the application, what queries are best suited for data retrieval, etc. - -Adding even a single column to a table definition requires modifying the SQL file, adding properties to the corresponding model, reflecting them in the database, updating documentation, etc. - -There are many other things to consider and correct. - -It is very difficult to keep up with all the maintenance during daily development, and even maintenance omissions may occur. - -I think the approach of using plain SQL to retrieve data without mapping table information to the application model and then retrieving the data with a specified type is a very good way to go. - -This way, there is no need to build database-specific models, because developers are free to work with data when they want to retrieve it, using the type of data they want to retrieve.
I also think it is very good at handling plain queries so that you can instantly see what queries are being executed. - -However, this method does not eliminate document updates, etc. just because table information is no longer managed in the application. - -LDBC has been developed to solve some of these problems. - -- Type safety: compile-time guarantees, development-time complements, read-time information -- Declarative: Separates the form of the table definition ("What") from the database connection ("How"). -- SchemaSPY Integration: Generate documents from table descriptions -- Libraries, not frameworks: can be integrated into your stack - -With LDBC, database information must be managed by the application, but type safety, query construction, and document management can be centralized. - -Mapping models in LDBC to table definitions is very easy. - -The mapping between the properties a model has and the data types defined for its columns is also very simple. The developer simply defines the corresponding columns in the same order as the properties the model has. - -```scala mdoc:silent -import ldbc.schema.* - -case class User( - id: Long, - name: String, - age: Option[Int], -) - -val table = Table[User]("user")( - column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), - column("name", VARCHAR(255)), - column("age", INT.UNSIGNED.DEFAULT(None)), -) -``` - -Also, attempting to combine the wrong types will result in a compile error. - -For example, passing a column of type INT to a column related to the name property of type String held by User will result in an error. - -```shell -[error] -- [E007] Type Mismatch Error: -[error] 169 | column("name", INT), -[error] | ^^^ -[error] |Found: ldbc.core.DataType.Integer[T] -[error] |Required: ldbc.core.DataType[String] -[error] | -[error] |where: T is a type variable with constraint <: Int | Long | Option[Int | Long] -``` - -For more information on these add-ons, see [Table Definitions](/ldbc/en/01-Table-Definitions.html). - -## Quick Start - -The current version is **$version$** for **Scala $scalaVersion$**. - -@@@ vars -```scala -libraryDependencies ++= Seq( - - // Start with this one. - "$org$" %% "ldbc-core" % "$version$", - - // Then add these as needed - "$org$" %% "ldbc-dsl" % "$version$", // Plain Query Database Connection - "$org$" %% "ldbc-query-builder" % "$version$", // Type-safe query construction - "$org$" %% "ldbc-schemaspy" % "$version$", // SchemaSPY document generation -) -``` -@@@ - -For more information on how to use the sbt plugin, please refer to this [documentation](/ldbc/en/07-Schema-Code-Generation.html). - -## TODO - -- JSON data type support -- SET data type support -- Geometry data type support -- Support for CHECK constraints -- Non-MySQL database support -- Streaming Support -- ZIO module support -- Integration with other database libraries -- Test Kit -- etc... From 508e1ddc65f61a9e6a069b30788bd7c3d955b226 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 20 Jul 2024 18:12:22 +0900 Subject: [PATCH 092/160] Update README --- README.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4500d2d26..7356e00c2 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,7 @@ ldbc (Lepus Database Connectivity) is Pure functional JDBC layer with Cats Effec ldbc is a [Typelevel](http://typelevel.org/) project. This means we embrace pure, typeful, functional programming, and provide a safe and friendly environment for teaching, learning, and contributing as described in the Scala [Code of Conduct](http://scala-lang.org/conduct.html). -ldbc is Created under the influence of [tapir](https://github.com/softwaremill/tapir), a declarative, type-safe web endpoint library. Using tapir, you can build type-safe endpoints and also generate OpenAPI documentation from the endpoints you build. - -ldbc allows the same type-safe construction with Scala at the database layer and document generation using the constructed one. +Note that **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. > [!NOTE] > **ldbc** is pre-1.0 software and is still undergoing active development. New versions are **not** binary compatible with prior versions, although in most cases user code will be source compatible. @@ -52,13 +50,13 @@ For people that want to skip the explanations and see it action, this is the pla ### Dependency Configuration ```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-dsl" % "${version}" +libraryDependencies += "io.github.takapi327" %% "ldbc-dsl" % "latest" ``` For Cross-Platform projects (JVM, JS, and/or Native): ```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-dsl" % "${version}" +libraryDependencies += "io.github.takapi327" %%% "ldbc-dsl" % "latest" ``` The dependency package used depends on whether the database connection is made via a connector using the Java API or a connector provided by ldbc. @@ -66,19 +64,19 @@ The dependency package used depends on whether the database connection is made v **Use jdbc connector** ```scala -libraryDependencies += "io.github.takapi327" %% "jdbc-connector" % "${version}" +libraryDependencies += "io.github.takapi327" %% "jdbc-connector" % "latest" ``` **Use ldbc connector** ```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-connector" % "${version}" +libraryDependencies += "io.github.takapi327" %% "ldbc-connector" % "latest" ``` For Cross-Platform projects (JVM, JS, and/or Native) ```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-connector" % "${version}" +libraryDependencies += "io.github.takapi327" %%% "ldbc-connector" % "latest" ``` ### Usage @@ -137,13 +135,13 @@ ldbc provides not only plain queries but also type-safe database connections usi The first step is to set up dependencies. ```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-query-builder" % "${version}" +libraryDependencies += "io.github.takapi327" %% "ldbc-query-builder" % "latest" ``` For Cross-Platform projects (JVM, JS, and/or Native): ```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-query-builder" % "${version}" +libraryDependencies += "io.github.takapi327" %%% "ldbc-query-builder" % "latest" ``` ldbc uses classes to construct queries. @@ -182,13 +180,13 @@ ldbc also allows type-safe construction of schema information for tables. The first step is to set up dependencies. ```scala -libraryDependencies += "io.github.takapi327" %% "ldbc-schema" % "${version}" +libraryDependencies += "io.github.takapi327" %% "ldbc-schema" % "latest" ``` For Cross-Platform projects (JVM, JS, and/or Native): ```scala -libraryDependencies += "io.github.takapi327" %%% "ldbc-schema" % "${version}" +libraryDependencies += "io.github.takapi327" %%% "ldbc-schema" % "latest" ``` The next step is to create a schema for use by the query builder. @@ -224,8 +222,8 @@ val result: IO[List[User]] = connection.use { conn => Full documentation can be found at Currently available in English and Japanese. -- [English](https://takapi327.github.io/ldbc/en/index.html) -- [Japanese](https://takapi327.github.io/ldbc/ja/index.html) +- [English](https://takapi327.github.io/ldbc/en/) +- [Japanese](https://takapi327.github.io/ldbc/ja/) ## Features/Roadmap From 1f0874c0acf92a39ebfe13ee191f12b47d001390 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 10 Aug 2024 16:55:59 +0900 Subject: [PATCH 093/160] Fixed document --- docs/src/main/mdoc/ja/reference/index.md | 4 ++-- docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md | 2 +- docs/src/main/mdoc/ja/tutorial/Setup.md | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/src/main/mdoc/ja/reference/index.md b/docs/src/main/mdoc/ja/reference/index.md index 10c12bbee..88da23fbf 100644 --- a/docs/src/main/mdoc/ja/reference/index.md +++ b/docs/src/main/mdoc/ja/reference/index.md @@ -5,10 +5,10 @@ # Reference -This section contains detailed discussions of ldbc's core abstractions and machinery. +このセクションでは、ldbcのコア抽象化と機械の詳細について説明します。 ## Table of Contents @:navigationTree { - entries = [ { target = "/en/reference", depth = 2 } ] + entries = [ { target = "/ja/reference", depth = 2 } ] } diff --git a/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md b/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md index 8c4b7df91..4a911d4a9 100644 --- a/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md +++ b/docs/src/main/mdoc/ja/tutorial/Parameterized-Queries.md @@ -60,7 +60,7 @@ SQLリテラルを扱う際によくあるイラつきは、一連の引数をIN val ids = NonEmptyList.of(1, 2, 3) connection.use { conn => - sql"SELECT name, email FROM user WHERE" ++ in("id", ids) + (sql"SELECT name, email FROM user WHERE" ++ in("id", ids)) .query[(String, String)] .to[List] .readOnly(conn) diff --git a/docs/src/main/mdoc/ja/tutorial/Setup.md b/docs/src/main/mdoc/ja/tutorial/Setup.md index d53dc564e..5faa2ae89 100644 --- a/docs/src/main/mdoc/ja/tutorial/Setup.md +++ b/docs/src/main/mdoc/ja/tutorial/Setup.md @@ -12,7 +12,6 @@ まず、Dockerを使用してデータベースを起動します。以下のコードを使用して、データベースを起動します。 ```yaml -version: '3' services: mysql: image: mysql:@MYSQL_VERSION@ @@ -108,15 +107,15 @@ brew install Virtuslab/scala-cli/scala-cli scala-cli https://github.com/takapi327/ldbc/tree/master/docs/src/main/scala/00-Setup.scala --dependency io.github.takapi327::ldbc-dsl:@VERSION@ --dependency io.github.takapi327::ldbc-connector:@VERSION@ ``` -次に、ldbcを依存関係に持つ新しいプロジェクトを作成します。 +### 最初のプログラム + +はじめに、ldbcを依存関係に持つ新しいプロジェクトを作成します。 ```scala //> using scala "@SCALA_VERSION@" //> using dep "@ORGANIZATION@::ldbc-dsl:@VERSION@" ``` -### 最初のプログラム - ldbcを使う前に、いくつかのシンボルをインポートする必要がある。ここでは便宜上、パッケージのインポートを使用する。これにより、高レベルAPIで作業する際に最もよく使用されるシンボルを得ることができる。 ```scala From da31bed9eb5f99e67ae6523e3cc31c3253dc9262 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 10 Aug 2024 17:14:56 +0900 Subject: [PATCH 094/160] Replace Connector document old -> reference --- .../main/mdoc/ja/reference/Connector.md} | 277 +++++++++--------- .../src/main/mdoc/ja/reference/directory.conf | 3 +- docs/src/main/mdoc/ja/reference/index.md | 2 +- 3 files changed, 144 insertions(+), 138 deletions(-) rename docs/{old/15-Connector.md => src/main/mdoc/ja/reference/Connector.md} (78%) diff --git a/docs/old/15-Connector.md b/docs/src/main/mdoc/ja/reference/Connector.md similarity index 78% rename from docs/old/15-Connector.md rename to docs/src/main/mdoc/ja/reference/Connector.md index 5fbfee04d..0cd48fdd9 100644 --- a/docs/old/15-Connector.md +++ b/docs/src/main/mdoc/ja/reference/Connector.md @@ -1,6 +1,11 @@ +{% + laika.title = コネクタ + laika.metadata.language = ja +%} + # コネクタ -この章では、LDBC独自のMySQLコネクタを使用したデータベース接続について説明します。 +この章では、ldbc独自のMySQLコネクタを使用したデータベース接続について説明します。 ScalaでMySQLデータベースへの接続を行うためにはJDBCを使用する必要があります。JDBCはJavaの標準APIであり、Scalaでも使用することができます。 JDBCはJavaで実装が行われているためScalaで使用する場合でもJVM環境でのみ動作することができます。 @@ -15,11 +20,11 @@ ScalaはJavaの資産を使用できるJVMのみで動作する言語から、 Typelevel Projectには[Skunk](https://github.com/typelevel/skunk)と呼ばれる[PostgreSQL](https://www.postgresql.org/)用のScalaライブラリが存在します。 このプロジェクトはJDBCを使用しておらず、純粋なScalaのみでPostgreSQLへの接続を実現しています。そのため、Skunkを使用すればJVM, JS, Native環境を問わずPostgreSQLへの接続を行うことができます。 -LDBC コネクタはこのSkunkに影響を受けてJVM, JS, Native環境を問わずMySQLへの接続を行えるようにするために開発が行われてるプロジェクトです。 +ldbc コネクタはこのSkunkに影響を受けてJVM, JS, Native環境を問わずMySQLへの接続を行えるようにするために開発が行われてるプロジェクトです。 ※ このコネクタは現在実験的な機能となります。そのため本番環境での使用しないでください。 -LDBCコネクタは一番低レイヤーのAPIとなります。 +ldbcコネクタは一番低レイヤーのAPIとなります。 今後このコネクタを使用してより高レイヤーのAPIを提供する予定です。また既存の高レイヤーのAPIとの互換性を持たせることも予定しています。 使用するにはプロジェクトに以下の依存関係を設定する必要があります。 @@ -27,7 +32,7 @@ LDBCコネクタは一番低レイヤーのAPIとなります。 **JVM** @@@ vars -```scala +```scala 3 libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" ``` @@@ @@ -35,7 +40,7 @@ libraryDependencies += "$org$" %% "ldbc-connector" % "$version$" **JS/Native** @@@ vars -```scala +```scala 3 libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" ``` @@@ @@ -52,14 +57,14 @@ libraryDependencies += "$org$" %%% "ldbc-connector" % "$version$" ## 接続 -LDBCコネクタを使用してMySQLへの接続を行うためには、`Connection`を使用します。 +ldbcコネクタを使用してMySQLへの接続を行うためには、`Connection`を使用します。 また、`Connection`はオブザーバビリティを意識した開発を行えるように`Otel4s`を使用してテレメトリデータを収集できるようにしています。 そのため、`Connection`を使用する際には`Otel4s`の`Tracer`を設定する必要があります。 開発時やトレースを使用したテレメトリデータが不要な場合は`Tracer.noop`を使用することを推奨します。 -```scala +```scala 3 import cats.effect.IO import org.typelevel.otel4s.trace.Tracer import ldbc.connector.Connection @@ -75,22 +80,22 @@ val connection = Connection[IO]( 以下は`Connection`構築時に設定できるプロパティの一覧です。 -| プロパティ | 型 | 用途 | -|-------------------------|--------------------|--------------------------------------------------------| -| host | String | MySQLサーバーのホストを指定します | -| port | Int | MySQLサーバーのポート番号を指定します | -| user | String | MySQLサーバーへログインを行うユーザー名を指定します | -| password | Option[String] | MySQLサーバーへログインを行うユーザーのパスワードを指定します | -| database | Option[String] | MySQLサーバーへ接続後に使用するデータベース名を指定します | -| debug | Boolean | 処理のログを出力します。デフォルトはfalseです | -| ssl | SSL | MySQLサーバーとの通知んでSSL/TLSを使用するかを指定します。デフォルトはSSL.Noneです | -| socketOptions | List[SocketOption] | TCP/UDP ソケットのソケットオプションを指定します。 | -| readTimeout | Duration | MySQLサーバーへの接続を試みるまでのタイムアウトを指定します。デフォルトはDuration.Infです。 | -| allowPublicKeyRetrieval | Boolean | MySQLサーバーとの認証時にRSA公開鍵を使用するかを指定します。デフォルトはfalseです。 | +| プロパティ | 型 | 用途 | +|---------------------------|----------------------|----------------------------------------------------------| +| `host` | `String` | `MySQLサーバーのホストを指定します` | +| `port` | `Int` | `MySQLサーバーのポート番号を指定します` | +| `user` | `String` | `MySQLサーバーへログインを行うユーザー名を指定します` | +| `password` | `Option[String]` | `MySQLサーバーへログインを行うユーザーのパスワードを指定します` | +| `database` | `Option[String]` | `MySQLサーバーへ接続後に使用するデータベース名を指定します` | +| `debug` | `Boolean` | `処理のログを出力します。デフォルトはfalseです` | +| `ssl` | `SSL` | `MySQLサーバーとの通知んでSSL/TLSを使用するかを指定します。デフォルトはSSL.Noneです` | +| `socketOptions` | `List[SocketOption]` | `TCP/UDP ソケットのソケットオプションを指定します。` | +| `readTimeout` | `Duration` | `MySQLサーバーへの接続を試みるまでのタイムアウトを指定します。デフォルトはDuration.Infです。` | +| `allowPublicKeyRetrieval` | `Boolean` | `MySQLサーバーとの認証時にRSA公開鍵を使用するかを指定します。デフォルトはfalseです。` | `Connection`は`Resource`を使用してリソース管理を行います。そのためコネクション情報を使用する場合は`use`メソッドを使用してリソースの管理を行います。 -```scala +```scala 3 connection.use { conn => // コードを記述 } @@ -102,7 +107,7 @@ MySQLでの認証は、クライアントがMySQLサーバーへ接続すると MySQLでサポートされている認証プラグインは[公式ページ](https://dev.mysql.com/doc/refman/8.0/ja/authentication-plugins.html)に記載されています。 -LDBCは現時点で以下の認証プラグインをサポートしています。 +ldbcは現時点で以下の認証プラグインをサポートしています。 - ネイティブプラガブル認証 - SHA-256 プラガブル認証 @@ -110,8 +115,8 @@ LDBCは現時点で以下の認証プラグインをサポートしています ※ ネイティブプラガブル認証とSHA-256 プラガブル認証はMySQL 8.xから非推奨となったプラグインです。特段理由がない場合はSHA-2 プラガブル認証のキャッシュを使用することを推奨します。 -LDBCのアプリケーションコード上で認証プラグインを意識する必要はありません。ユーザーはMySQLのデータベース上で使用したい認証プラグインで作成されたユーザーを作成し、LDBCのアプリケーションコード上ではそのユーザーを使用してMySQLへの接続を試みるだけで問題ありません。 -LDBCが内部で認証プラグインを判断し、適切な認証プラグインを使用してMySQLへの接続を行います。 +ldbcのアプリケーションコード上で認証プラグインを意識する必要はありません。ユーザーはMySQLのデータベース上で使用したい認証プラグインで作成されたユーザーを作成し、ldbcのアプリケーションコード上ではそのユーザーを使用してMySQLへの接続を試みるだけで問題ありません。 +ldbcが内部で認証プラグインを判断し、適切な認証プラグインを使用してMySQLへの接続を行います。 ## 実行 @@ -139,7 +144,7 @@ CREATE TABLE users ( クエリを実行した結果MySQLサーバーから返される値は`ResultSet`に格納されて戻り値として返却されます。 -```scala +```scala 3 3 connection.use { conn => for statement <- conn.createStatement() @@ -155,7 +160,7 @@ connection.use { conn => クエリを実行した結果MySQLサーバーから返される値は影響を受けた行数が戻り値として返却されます。 -```scala +```scala 3 connection.use { conn => for statement <- conn.createStatement() @@ -170,7 +175,7 @@ connection.use { conn => クエリを実行した結果MySQLサーバーから返される値はAUTO_INCREMENTに生成された値が戻り値として返却されます。 -```scala +```scala 3 connection.use { conn => for statement <- conn.createStatement() @@ -182,7 +187,7 @@ connection.use { conn => ### Client/Server PreparedStatement -LDBCでは`PreparedStatement`を`Client PreparedStatement`と`Server PreparedStatement`に分けて提供しています。 +ldbcでは`PreparedStatement`を`Client PreparedStatement`と`Server PreparedStatement`に分けて提供しています。 `Client PreparedStatement`は動的なパラメーターを使用してアプリケーション上でSQLの構築を行い、MySQLサーバーに送信を行うためのAPIです。 そのためMySQLサーバーへのクエリ送信方法は`Statement`と同じになります。 @@ -205,7 +210,7 @@ LDBCでは`PreparedStatement`を`Client PreparedStatement`と`Server PreparedSta `Connection`の`clientPreparedStatement`メソッドを使用して`Client PreparedStatement`を構築します。 -```scala +```scala 3 connection.use { conn => for statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") @@ -218,7 +223,7 @@ connection.use { conn => `Connection`の`serverPreparedStatement`メソッドを使用して`Server PreparedStatement`を構築します。 -```scala +```scala 3 connection.use { conn => for statement <- conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") @@ -233,7 +238,7 @@ connection.use { conn => クエリを実行した結果MySQLサーバーから返される値は`ResultSet`に格納されて戻り値として返却されます。 -```scala +```scala 3 connection.use { conn => for statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") @@ -249,30 +254,30 @@ connection.use { conn => `setXXX`メソッドはパラメーターのインデックスとパラメーターの値を指定します。 -```scala +```scala 3 statement.setLong(1, 1) ``` 現在のバージョンでは以下のメソッドがサポートされています。 -| メソッド | 型 | 備考 | -|---------------|-------------------------------------|-----------------------------------| -| setNull | | パラメーターにNULLをセットします | -| setBoolean | Boolean/Option[Boolean] | | -| setByte | Byte/Option[Byte] | | -| setShort | Short/Option[Short] | | -| setInt | Int/Option[Int] | | -| setLong | Long/Option[Long] | | -| setBigInt | BigInt/Option[BigInt] | | -| setFloat | Float/Option[Float] | | -| setDouble | Double/Option[Double] | | -| setBigDecimal | BigDecimal/Option[BigDecimal] | | -| setString | String/Option[String] | | -| setBytes | Array[Byte]/Option[Array[Byte]] | | -| setDate | LocalDate/Option[LocalDate] | `java.sql`ではなく`java.time`を直接扱います。 | -| setTime | LocalTime/Option[LocalTime] | `java.sql`ではなく`java.time`を直接扱います。 | -| setTimestamp | LocalDateTime/Option[LocalDateTime] | `java.sql`ではなく`java.time`を直接扱います。 | -| setYear | Year/Option[Year] | `java.sql`ではなく`java.time`を直接扱います。 | +| メソッド | 型 | 備考 | +|-----------------|---------------------------------------|-----------------------------------| +| `setNull` | | パラメーターにNULLをセットします | +| `setBoolean` | `Boolean/Option[Boolean]` | | +| `setByte` | `Byte/Option[Byte]` | | +| `setShort` | `Short/Option[Short]` | | +| `setInt` | `Int/Option[Int]` | | +| `setLong` | `Long/Option[Long]` | | +| `setBigInt` | `BigInt/Option[BigInt]` | | +| `setFloat` | `Float/Option[Float]` | | +| `setDouble` | `Double/Option[Double]` | | +| `setBigDecimal` | `BigDecimal/Option[BigDecimal]` | | +| `setString` | `String/Option[String]` | | +| `setBytes` | `Array[Byte]/Option[Array[Byte]]` | | +| `setDate` | `LocalDate/Option[LocalDate]` | `java.sql`ではなく`java.time`を直接扱います。 | +| `setTime` | `LocalTime/Option[LocalTime]` | `java.sql`ではなく`java.time`を直接扱います。 | +| `setTimestamp` | `LocalDateTime/Option[LocalDateTime]` | `java.sql`ではなく`java.time`を直接扱います。 | +| `setYear` | `Year/Option[Year]` | `java.sql`ではなく`java.time`を直接扱います。 | #### 書き込みクエリ @@ -280,7 +285,7 @@ statement.setLong(1, 1) クエリを実行した結果MySQLサーバーから返される値は影響を受けた行数が戻り値として返却されます。 -```scala +```scala 3 connection.use { conn => for statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") @@ -298,7 +303,7 @@ connection.use { conn => クエリを実行した結果MySQLサーバーから返される値はAUTO_INCREMENTに生成された値が戻り値として返却されます。 -```scala +```scala 3 connection.use { conn => for statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) // or conn.serverPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS) @@ -314,7 +319,7 @@ connection.use { conn => `ResultSet`はクエリ実行後にMySQLサーバーから返された値を格納するためのAPIです。 -SQLを実行して取得したレコードを`ResultSet`から取得するにはJDBCと同じように`next`メソッドと`getXXX`メソッドを使用して取得する方法と、LDBC独自の`decode`メソッドを使用する方法があります。 +SQLを実行して取得したレコードを`ResultSet`から取得するにはJDBCと同じように`next`メソッドと`getXXX`メソッドを使用して取得する方法と、ldbc独自の`decode`メソッドを使用する方法があります。 #### next/getXXX @@ -324,7 +329,7 @@ SQLを実行して取得したレコードを`ResultSet`から取得するには `getXXX`メソッドは取得するカラムのインデックスを指定する方法とカラム名を指定する方法があります。 -```scala +```scala 3 connection.use { conn => for statement <- conn.clientPreparedStatement("SELECT `id`, `name`, `age` FROM users WHERE id = ?") @@ -349,7 +354,7 @@ connection.use { conn => 例では、usersテーブルのid, name, ageカラムを取得する場合を示しておりそれぞれのカラムの型を指定しています。 -```scala +```scala 3 result.decode(bigint *: varchar *: int.opt) ``` @@ -358,7 +363,7 @@ NULL許容のカラムを取得する場合は`Option`型に変換するため クエリ実行からレコード取得までの一連の流れは以下のようになります。 -```scala +```scala 3 connection.use { conn => for statement <- conn.clientPreparedStatement("SELECT * FROM users WHERE id = ?") // or conn.serverPreparedStatement("SELECT * FROM users WHERE id = ?") @@ -376,42 +381,42 @@ connection.use { conn => 現在のバージョンでは以下のデータ型がサポートされています。 -| Codec | データ型 | Scala 型 | -|-------------|-------------------|----------------| -| boolean | BOOLEAN | Boolean | -| tinyint | TINYINT | Byte | -| utinyint | unsigned TINYINT | Short | -| smallint | SMALLINT | Short | -| usmallint | unsigned SMALLINT | Int | -| int | INT | Int | -| uint | unsigned INT | Long | -| bigint | BIGINT | Long | -| ubigint | unsigned BIGINT | BigInt | -| float | FLOAT | Float | -| double | DOUBLE | Double | -| decimal | DECIMAL | BigDecimal | -| char | CHAR | String | -| varchar | VARCHAR | String | -| binary | BINARY | Array[Byte] | -| varbinary | VARBINARY | String | -| tinyblob | TINYBLOB | String | -| blob | BLOB | String | -| mediumblob | MEDIUMBLOB | String | -| longblob | LONGBLOB | String | -| tinytext | TINYTEXT | String | -| text | TEXT | String | -| mediumtext | MEDIUMTEXT | String | -| longtext | LONGTEXT | String | -| enum | ENUM | String | -| set | SET | List[String] | -| json | JSON | String | -| date | DATE | LocalDate | -| time | TIME | LocalTime | -| timetz | TIME | OffsetTime | -| datetime | DATETIME | LocalDateTime | -| timestamp | TIMESTAMP | LocalDateTime | -| timestamptz | TIMESTAMP | OffsetDateTime | -| year | YEAR | Year | +| Codec | データ型 | Scala 型 | +|---------------|---------------------|------------------| +| `boolean` | `BOOLEAN` | `Boolean` | +| `tinyint` | `TINYINT` | `Byte` | +| `utinyint` | `unsigned TINYINT` | `Short` | +| `smallint` | `SMALLINT` | `Short` | +| `usmallint` | `unsigned SMALLINT` | `Int` | +| `int` | `INT` | `Int` | +| `uint` | `unsigned INT` | `Long` | +| `bigint` | `BIGINT` | `Long` | +| `ubigint` | `unsigned BIGINT` | `BigInt` | +| `float` | `FLOAT` | `Float` | +| `double` | `DOUBLE` | `Double` | +| `decimal` | `DECIMAL` | `BigDecimal` | +| `char` | `CHAR` | `String` | +| `varchar` | `VARCHAR` | `String` | +| `binary` | `BINARY` | `Array[Byte]` | +| `varbinary` | `VARBINARY` | `String` | +| `tinyblob` | `TINYBLOB` | `String` | +| `blob` | `BLOB` | `String` | +| `mediumblob` | `MEDIUMBLOB` | `String` | +| `longblob` | `LONGBLOB` | `String` | +| `tinytext` | `TINYTEXT` | `String` | +| `text` | `TEXT` | `String` | +| `mediumtext` | `MEDIUMTEXT` | `String` | +| `longtext` | `LONGTEXT` | `String` | +| `enum` | `ENUM` | `String` | +| `set` | `SET` | `List[String]` | +| `json` | `JSON` | `String` | +| `date` | `DATE` | `LocalDate` | +| `time` | `TIME` | `LocalTime` | +| `timetz` | `TIME` | `OffsetTime` | +| `datetime` | `DATETIME` | `LocalDateTime` | +| `timestamp` | `TIMESTAMP` | `LocalDateTime` | +| `timestamptz` | `TIMESTAMP` | `OffsetDateTime` | +| `year` | `YEAR` | `Year` | ※ 現在MySQLのデータ型を指定して値を取得するような作りとなっていますが、将来的にはより簡潔にScalaの型を指定して値を取得するような作りに変更する可能性があります。 @@ -432,13 +437,13 @@ connection.use { conn => まず、`setAutoCommit`メソッドを使用してトランザクションの自動コミットを無効にします。 -```scala +```scala 3 conn.setAutoCommit(false) ``` 何かしらの処理を行った後に`commit`メソッドを使用してトランザクションをコミットします。 -```scala +```scala 3 for statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") _ <- statement.setString(1, "Alice") @@ -449,7 +454,7 @@ yield ``` もしくは、`rollback`メソッドを使用してトランザクションをロールバックします。 -```scala +```scala 3 for statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") _ <- statement.setString(1, "Alice") @@ -463,7 +468,7 @@ yield ### トランザクション分離レベル -LDBCではトランザクション分離レベルの設定を行うことができます。 +ldbcではトランザクション分離レベルの設定を行うことができます。 トランザクション分離レベルは`setTransactionIsolation`メソッドを使用して設定を行います。 @@ -476,7 +481,7 @@ MySQLでは以下のトランザクション分離レベルがサポートされ MySQLのトランザクション分離レベルについては[公式ドキュメント](https://dev.mysql.com/doc/refman/8.0/ja/innodb-transaction-isolation-levels.html)を参照してください。 -```scala +```scala 3 import ldbc.connector.Connection.TransactionIsolationLevel conn.setTransactionIsolation(TransactionIsolationLevel.REPEATABLE_READ) @@ -484,7 +489,7 @@ conn.setTransactionIsolation(TransactionIsolationLevel.REPEATABLE_READ) 現在設定されているトランザクション分離レベルを取得するには`getTransactionIsolation`メソッドを使用します。 -```scala +```scala 3 for isolationLevel <- conn.getTransactionIsolation() yield @@ -511,7 +516,7 @@ Savepointの名前を指定しない場合、デフォルトの名前としてUU ※ MySQLではデフォルトで自動コミットが有効になっているため、Savepointを使用する場合は自動コミットを無効にする必要があります。そうしないと全ての処理が都度コミットされてしまうため、Savepointを使用したトランザクションのロールバックを行うことができなくなるためです。 -```scala +```scala 3 for _ <- conn.setAutoCommit(false) savepoint <- conn.setSavepoint("savepoint1") @@ -523,7 +528,7 @@ yield savepoint.getSavepointName Savepointを使用してトランザクションの一部をロールバックするには、`rollback`メソッドにSavepointを渡すことでロールバックを行います。 Savepointを使用して部分的にロールバックをした後、トランザクション全体をコミットするとそのSavepoint以降のトランザクションはコミットされません。 -```scala +```scala 3 for _ <- conn.setAutoCommit(false) savepoint <- conn.setSavepoint("savepoint1") @@ -537,7 +542,7 @@ yield Savepointをリリースするには、`releaseSavepoint`メソッドにSavepointを渡すことでリリースを行います。 Savepointをリリースした後、トランザクション全体をコミットするとそのSavepoint以降のトランザクションはコミットされます。 -```scala +```scala 3 for _ <- conn.setAutoCommit(false) savepoint <- conn.setSavepoint("savepoint1") @@ -550,29 +555,29 @@ yield MySQLにはいくつかのユーティリティコマンドがあります。([参照](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase_utility.html)) -LDBCではこれらのコマンドを使用するためのAPIを提供しています。 +ldbcではこれらのコマンドを使用するためのAPIを提供しています。 -| コマンド | 用途 | サポート | -|----------------------|------------------------------------|------| -| COM_QUIT | クライアントが接続を閉じることをサーバーに要求していることを伝える。 | ✅ | -| COM_INIT_DB | 接続のデフォルト・スキーマを変更する | ✅ | -| COM_STATISTICS | 内部ステータスの文字列を可読形式で取得する。 | ✅ | -| COM_DEBUG | サーバーの標準出力にデバッグ情報をダンプする | ❌ | -| COM_PING | サーバーが生きているかチェックする | ✅ | -| COM_CHANGE_USER | 現在の接続のユーザーを変更する | ✅ | -| COM_RESET_CONNECTION | セッションの状態をリセットする | ✅ | -| COM_SET_OPTION | 現在の接続のオプションを設定する | ✅ | +| コマンド | 用途 | サポート | +|------------------------|--------------------------------------|------| +| `COM_QUIT` | `クライアントが接続を閉じることをサーバーに要求していることを伝える。` | ✅ | +| `COM_INIT_DB` | `接続のデフォルト・スキーマを変更する` | ✅ | +| `COM_STATISTICS` | `内部ステータスの文字列を可読形式で取得する。` | ✅ | +| `COM_DEBUG` | `サーバーの標準出力にデバッグ情報をダンプする` | ❌ | +| `COM_PING` | `サーバーが生きているかチェックする` | ✅ | +| `COM_CHANGE_USER` | `現在の接続のユーザーを変更する` | ✅ | +| `COM_RESET_CONNECTION` | `セッションの状態をリセットする` | ✅ | +| `COM_SET_OPTION` | `現在の接続のオプションを設定する` | ✅ | ### COM_QUIT `COM_QUIT`はクライアントが接続を閉じることをサーバーに要求していることを伝えるためのコマンドです。 -LDBCでは`Connection`の`close`メソッドを使用して接続を閉じることができます。 +ldbcでは`Connection`の`close`メソッドを使用して接続を閉じることができます。 `close`メソッドを使用すると接続が閉じられるため、その後の処理で接続を使用することはできません。 ※ `Connection`は`Resource`を使用してリソース管理を行います。そのため`close`メソッドを使用してリソースの解放を行う必要はありません。 -```scala +```scala 3 connection.use { conn => conn.close() } @@ -582,9 +587,9 @@ connection.use { conn => `COM_INIT_DB`は接続のデフォルト・スキーマを変更するためのコマンドです。 -LDBCでは`Connection`の`setSchema`メソッドを使用してデフォルト・スキーマを変更することができます。 +ldbcでは`Connection`の`setSchema`メソッドを使用してデフォルト・スキーマを変更することができます。 -```scala +```scala 3 connection.use { conn => conn.setSchema("test") } @@ -594,9 +599,9 @@ connection.use { conn => `COM_STATISTICS`は内部ステータスの文字列を可読形式で取得するためのコマンドです。 -LDBCでは`Connection`の`getStatistics`メソッドを使用して内部ステータスの文字列を取得することができます。 +ldbcでは`Connection`の`getStatistics`メソッドを使用して内部ステータスの文字列を取得することができます。 -```scala +```scala 3 connection.use { conn => conn.getStatistics } @@ -617,10 +622,10 @@ connection.use { conn => `COM_PING`はサーバーが生きているかチェックするためのコマンドです。 -LDBCでは`Connection`の`isValid`メソッドを使用してサーバーが生きているかチェックすることができます。 +ldbcでは`Connection`の`isValid`メソッドを使用してサーバーが生きているかチェックすることができます。 サーバーが生きている場合は`true`を返却し、生きていない場合は`false`を返却します。 -```scala +```scala 3 connection.use { conn => conn.isValid } @@ -636,9 +641,9 @@ connection.use { conn => - プリペアド・ステートメント - etc... -LDBCでは`Connection`の`changeUser`メソッドを使用してユーザーを変更することができます。 +ldbcでは`Connection`の`changeUser`メソッドを使用してユーザーを変更することができます。 -```scala +```scala 3 connection.use { conn => conn.changeUser("root", "password") } @@ -653,9 +658,9 @@ connection.use { conn => - 再認証を行わない(そのために余分なクライアント/サーバ交換を行わない)。 - 接続を閉じない。 -LDBCでは`Connection`の`resetServerState`メソッドを使用してセッションの状態をリセットすることができます。 +ldbcでは`Connection`の`resetServerState`メソッドを使用してセッションの状態をリセットすることができます。 -```scala +```scala 3 connection.use { conn => conn.resetServerState } @@ -665,14 +670,14 @@ connection.use { conn => `COM_SET_OPTION`は現在の接続のオプションを設定するためのコマンドです。 -LDBCでは`Connection`の`enableMultiQueries`メソッドと`disableMultiQueries`メソッドを使用してオプションを設定することができます。 +ldbcでは`Connection`の`enableMultiQueries`メソッドと`disableMultiQueries`メソッドを使用してオプションを設定することができます。 `enableMultiQueries`メソッドを使用すると、複数のクエリを一度に実行することができます。 `disableMultiQueries`メソッドを使用すると、複数のクエリを一度に実行することができなくなります。 ※ これは、Insert、Update、および Delete ステートメントによるバッチ処理にのみ使用できます。Selectステートメントで使用を行なったとしても、最初のクエリの結果のみが返されます。 -```scala +```scala 3 connection.use { conn => conn.enableMultiQueries *> conn.disableMultiQueries } @@ -680,12 +685,12 @@ connection.use { conn => ## バッチコマンド -LDBCではバッチコマンドを使用して複数のクエリを一度に実行することができます。 +ldbcではバッチコマンドを使用して複数のクエリを一度に実行することができます。 バッチコマンドを使用することで、複数のクエリを一度に実行することができるため、ネットワークラウンドトリップの回数を減らすことができます。 バッチコマンドを使用するには`Statement`または`PreparedStatement`の`addBatch`メソッドを使用してクエリを追加し、`executeBatch`メソッドを使用してクエリを実行します。 -```scala 3 +```scala 3 3 connection.use { conn => for statement <- conn.createStatement() @@ -711,7 +716,7 @@ INSERT INTO users (name, age) VALUES ('Alice', 20);INSERT INTO users (name, age) 手動でクリアする場合は`clearBatch`メソッドを使用してクリアを行います。 -```scala +```scala 3 connection.use { conn => for statement <- conn.createStatement() @@ -734,7 +739,7 @@ connection.use { conn => 例えば、以下のクエリをバッチコマンドで実行した場合、`Statement`を使用しているため、複数のクエリが一度に実行されます。 -```scala +```scala 3 connection.use { conn => for statement <- conn.createStatement() @@ -750,7 +755,7 @@ connection.use { conn => しかし、以下のクエリをバッチコマンドで実行した場合、`PreparedStatement`を使用しているため、1つのクエリが実行されます。 -```scala +```scala 3 connection.use { conn => for statement <- conn.clientPreparedStatement("INSERT INTO users (name, age) VALUES (?, ?)") @@ -772,7 +777,7 @@ connection.use { conn => ## ストアドプロシージャの実行 -LDBCではストアドプロシージャを実行するためのAPIを提供しています。 +ldbcではストアドプロシージャを実行するためのAPIを提供しています。 ストアドプロシージャを実行するには`Connection`の`prepareCall`メソッドを使用して`CallableStatement`を構築します。 @@ -793,7 +798,7 @@ END 上記のストアドプロシージャを実行する場合は以下のようになります。 -```scala +```scala 3 connection.use { conn => for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") @@ -813,11 +818,11 @@ connection.use { conn => } ``` -出力パラメータ(ストアド・プロシージャを作成したときにOUTまたはINOUTとして指定したパラメータ)の値を取得するには、JDBCでは、CallableStatementインターフェイスのさまざまな`registerOutputParameter()`メソッドを使用して、ステートメント実行前にパラメータを指定する必要がありますが、LDBCでは`setXXX`メソッドを使用してパラメータを設定することだけクエリ実行時にパラメーターの設定も行なってくれます。 +出力パラメータ(ストアド・プロシージャを作成したときにOUTまたはINOUTとして指定したパラメータ)の値を取得するには、JDBCでは、CallableStatementインターフェイスのさまざまな`registerOutputParameter()`メソッドを使用して、ステートメント実行前にパラメータを指定する必要がありますが、ldbcでは`setXXX`メソッドを使用してパラメータを設定することだけクエリ実行時にパラメーターの設定も行なってくれます。 -ただし、LDBCでも`registerOutputParameter()`メソッドを使用してパラメータを指定することもできます。 +ただし、ldbcでも`registerOutputParameter()`メソッドを使用してパラメータを指定することもできます。 -```scala +```scala 3 connection.use { conn => for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") @@ -834,7 +839,7 @@ connection.use { conn => ## 未対応機能 -LDBCコネクタは現在実験的な機能となります。そのため、以下の機能はサポートされていません。 +ldbcコネクタは現在実験的な機能となります。そのため、以下の機能はサポートされていません。 機能提供は順次行っていく予定です。 - コネクションプーリング diff --git a/docs/src/main/mdoc/ja/reference/directory.conf b/docs/src/main/mdoc/ja/reference/directory.conf index 721762fb6..104460f08 100644 --- a/docs/src/main/mdoc/ja/reference/directory.conf +++ b/docs/src/main/mdoc/ja/reference/directory.conf @@ -1,4 +1,5 @@ laika.title = Reference laika.navigationOrder = [ - index.md + index.md, + Connector.md ] diff --git a/docs/src/main/mdoc/ja/reference/index.md b/docs/src/main/mdoc/ja/reference/index.md index 88da23fbf..cc5b27567 100644 --- a/docs/src/main/mdoc/ja/reference/index.md +++ b/docs/src/main/mdoc/ja/reference/index.md @@ -1,5 +1,5 @@ {% - laika.title = Intro + laika.title = はじめに laika.metadata.language = ja %} From 5b4c7b1618dfb4999959a2ff6e24b6f355ed78cb Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sat, 10 Aug 2024 18:29:10 +0900 Subject: [PATCH 095/160] Delete unused --- .../12-Generating-SchemaSPY-Documentation.md | 76 ------------------- docs/old/14-Perdormance.md | 39 ---------- 2 files changed, 115 deletions(-) delete mode 100644 docs/old/12-Generating-SchemaSPY-Documentation.md delete mode 100644 docs/old/14-Perdormance.md diff --git a/docs/old/12-Generating-SchemaSPY-Documentation.md b/docs/old/12-Generating-SchemaSPY-Documentation.md deleted file mode 100644 index 78c697cb5..000000000 --- a/docs/old/12-Generating-SchemaSPY-Documentation.md +++ /dev/null @@ -1,76 +0,0 @@ -# SchemaSPYドキュメントの生成 - -この章では、LDBCで構築したテーブル定義を使用して、SchemaSPYドキュメントの作成を行うための方法について説明します。 - -プロジェクトに以下の依存関係を設定する必要があります。 - -@@@ vars -```scala -libraryDependencies += "$org$" %% "ldbc-schemaspy" % "$version$" -``` -@@@ - -LDBCでのテーブル定義方法をまだ読んでいない場合は、[テーブル定義](/ldbc/ja/01-Table-Definitions.html)の章を先に読むことをオススメしましす。 - -以下のコード例では、以下のimportを想定しています。 - -```scala 3 -import ldbc.core.* -import ldbc.schemaspy.SchemaSpyGenerator -``` - -## テーブル定義から生成 - -SchemaSPYはデータベースへ接続を行いMeta情報やテーブル構造を取得しその情報を元にドキュメントを生成しますが、LDBCではデータベースへの接続は行わずLDBCで構築したテーブル構造を使用してSchemaSPYのドキュメントを生成します。 -データベースへの接続を行わないためシンプルにSchemaSPYを使用して生成したドキュメントと乖離する項目があります。例えば、現在テーブルに保存されているレコード数などの情報は表示することができません。 - -ドキュメントを生成するためにはデータベースの情報が必要です。LDBCではデータベースの情報を表現するためのtraitが存在しています。 - -`ldbc.core.Database`を使用してデータベース情報を構築したサンプルは以下になります。 - -```scala 3 -case class SampleLdbcDatabase( - schemaMeta: Option[String] = None, - catalog: Option[String] = Some("def"), - host: String = "127.0.0.1", - port: Int = 3306 -) extends Database: - - override val databaseType: Database.Type = Database.Type.MySQL - - override val name: String = "sample_ldbc" - - override val schema: String = "sample_ldbc" - - override val character: Option[Character] = None - - override val collate: Option[Collate] = None - - override val tables = Set( - ... // LDBCで構築したテーブル構造を列挙 - ) -``` - -SchemaSPYのドキュメント生成には`SchemaSpyGenerator`を使用します。生成したデータベース定義を`default`メソッドに渡し、`generate`を呼び出すと第2引数に指定したファイルの場所にSchemaSPYのファイル群が生成されます。 - -```scala 3 -@main -def run(): Unit = - val file = java.io.File("document") - SchemaSpyGenerator.default(SampleLdbcDatabase(), file).generate() -``` - -生成されたファイルの`index.html`を開くとSchemaSPYのドキュメントを確認することができます。 - -## データベース接続から生成 - -SchemaSpyGeneratorには`connect`メソッドも存在しています。こちらは標準のSchemaSpyの生成方法と同様にデータベースに接続を行いドキュメントの生成を行います。 - -```scala 3 -@main -def run(): Unit = - val file = java.io.File("document") - SchemaSpyGenerator.connect(SampleLdbcDatabase(), "user name", "password" file).generate() -``` - -データベース接続を行う処理はSchemaSpy内部のJavaで書かれた実装で行われます。そのためEffectシステムでスレッドなどが管理されていないことに注意してください。 diff --git a/docs/old/14-Perdormance.md b/docs/old/14-Perdormance.md deleted file mode 100644 index af27a61f4..000000000 --- a/docs/old/14-Perdormance.md +++ /dev/null @@ -1,39 +0,0 @@ -# パフォーマンス - -## コンパイル時間のオーバーヘッド - -テーブル定義のコンパイル時間はカラムの数に応じて増加する - -