Schema Code Generation

In Schema, we learned how to define schemas using Scala code. However, when working with existing databases, manually defining schemas can be time-consuming and prone to errors. This page explains how to automatically generate Scala code from existing SQL files.

Code generation is a powerful tool for automating repetitive tasks and reducing human errors. ldbc provides functionality to automatically generate model classes and table definitions from SQL files.

SBT Plugin Setup

Adding the Plugin

You need to set up the following dependency in your project. Add it to project/plugins.sbt.

addSbtPlugin("io.github.takapi327" % "ldbc-plugin" % "0.3.0-beta11")

Enabling the Plugin

Enable the plugin for your project in the build.sbt file.

lazy val root = (project in file("."))
  .enablePlugins(Ldbc)

Basic Usage

Specifying SQL Files

Configure the SQL files to be parsed. You can specify a single file or multiple files.

// Specifying a single SQL file
Compile / parseFiles := List(
  baseDirectory.value / "sql" / "schema.sql"
)

// Specifying multiple SQL files
Compile / parseFiles := List(
  baseDirectory.value / "sql" / "users.sql",
  baseDirectory.value / "sql" / "products.sql"
)

Specifying Directories

To target all SQL files in a specific directory, use parseDirectories.

// Specify by directory
Compile / parseDirectories := List(
  baseDirectory.value / "sql"
)

Generated Code

After configuration, code will be automatically generated when you compile with sbt.

sbt compile

The generated files are stored in the target/scala-X.X/src_managed/main directory.

Manual Generation

If you want to force code generation without using cache, use the following command:

sbt generateBySchema

SQL File Format Requirements

SQL files must include the following elements:

Database Definition

At the beginning of the file, always include a Create or Use statement for the database. This determines the package name and table ownership in the generated code.

-- Method 1: Creating a database
CREATE DATABASE `my_app`;

-- Or Method 2: Using an existing database
USE `my_app`;

Table Definitions

After the database definition, include table definitions.

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(255) NOT NULL,
  `email` VARCHAR(255) NOT NULL UNIQUE,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Complete SQL file example:

CREATE DATABASE `my_app`;

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(255) NOT NULL,
  `email` VARCHAR(255) NOT NULL UNIQUE,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

DROP TABLE IF EXISTS `products`;
CREATE TABLE `products` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(255) NOT NULL,
  `price` DECIMAL(10, 2) NOT NULL,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Detailed Configuration Options

The ldbc plugin offers the following configuration keys to customize code generation.

Configuration Key List

Key Default Value Details
parseFiles List.empty List of SQL files to parse
parseDirectories List.empty Directory-based specification of SQL files to parse
excludeFiles List.empty List of filenames to exclude from parsing
customYamlFiles List.empty List of YAML files for type customization
classNameFormat Format.PASCAL Format for generated class names (PASCAL, CAMEL, SNAKE)
propertyNameFormat Format.CAMEL Format for generated property names (PASCAL, CAMEL, SNAKE)
ldbcPackage ldbc.generated Package name for generated files

Example: Detailed Configuration

Compile / parseFiles := List(
  baseDirectory.value / "sql" / "schema.sql"
)

Compile / parseDirectories := List(
  baseDirectory.value / "sql" / "tables"
)

Compile / excludeFiles := List(
  "temp_tables.sql", "test_data.sql"
)

Compile / classNameFormat := PASCAL // PascalCase (MyClass)
Compile / propertyNameFormat := CAMEL // camelCase (myProperty)

Compile / ldbcPackage := "com.example.db"

Example of Generated Code

For example, with an SQL file like:

CREATE DATABASE `shop`;

CREATE TABLE `products` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(255) NOT NULL,
  `price` DECIMAL(10, 2) NOT NULL,
  `description` TEXT,
  `category_id` INT NOT NULL,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

The following Scala code would be generated:

package com.example.db

import ldbc.schema.*

import java.time.LocalDateTime

// Model class
case class Product(
  id: Long,
  name: String,
  price: BigDecimal,
  description: Option[String],
  categoryId: Int,
  createdAt: LocalDateTime
)

// Table definition and query builder
object Product {
  val table = TableQuery[ProductTable]
  
  class ProductTable extends Table[Product]("products"):
    def id: Column[Long] = column[Long]("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY)
    def name: Column[String] = column[String]("name", VARCHAR(255), NOT_NULL)
    def price: Column[BigDecimal] = column[BigDecimal]("price", DECIMAL(10, 2), NOT_NULL)
    def description: Column[Option[String]] = column[Option[String]]("description", TEXT)
    def categoryId: Column[Int] = column[Int]("category_id", INT, NOT_NULL)
    def createdAt: Column[LocalDateTime] = column[LocalDateTime]("created_at", TIMESTAMP, NOT_NULL, DEFAULT_CURRENT_TIMESTAMP)
    
    override def * : Column[Product] = (id *: name *: price *: description *: categoryId *: createdAt).to[Product]
}

Customizing Types

If you want to change the types in the automatically generated code to your own types, you can customize them using YAML files.

YAML File Configuration

First, create a YAML file for customization.

# custom_types.yml
database:
  name: 'shop'
  tables:
    - name: 'products'
      columns:
        - name: 'category_id'
          type: 'ProductCategory'
      object:
        extends:
          - 'com.example.ProductTypeMapping'

Then, add this YAML file to your project configuration.

Compile / customYamlFiles := List(
  baseDirectory.value / "config" / "custom_types.yml"
)

Custom Type Implementation

Next, implement the custom type conversion referenced in the YAML file.

// com/example/ProductTypeMapping.scala
package com.example

import ldbc.dsl.Codec

sealed trait ProductCategory {
  def id: Int
}

object ProductCategory {
  case object Electronics extends ProductCategory { val id = 1 }
  case object Books extends ProductCategory { val id = 2 }
  case object Clothing extends ProductCategory { val id = 3 }
  
  def fromId(id: Int): ProductCategory = id match {
    case 1 => Electronics
    case 2 => Books
    case 3 => Clothing
    case _ => throw new IllegalArgumentException(s"Unknown category ID: $id")
  }
}

trait ProductTypeMapping {
  given Codec[ProductCategory] = Codec[Int].imap(ProductCategory.fromId)(_.id)
}

Generated Code After Customization

With the above configuration, code like the following would be generated:

package ldbc.generated.shop

import ldbc.schema.*
import java.time.LocalDateTime
import com.example.ProductCategory

case class Product(
  id: Long,
  name: String,
  price: BigDecimal,
  description: Option[String],
  categoryId: ProductCategory, // Changed to custom type
  createdAt: LocalDateTime
)

object Product extends com.example.ProductTypeMapping {
  val table = TableQuery[ProductTable]
  
  class ProductTable extends Table[Product]("products"):
    def id: Column[Long] = column[Long]("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY)
    def name: Column[String] = column[String]("name", VARCHAR(255), NOT_NULL)
    def price: Column[BigDecimal] = column[BigDecimal]("price", DECIMAL(10, 2), NOT_NULL)
    def description: Column[Option[String]] = column[Option[String]]("description", TEXT)
    def categoryId: Column[Int] = column[Int]("category_id", INT, NOT_NULL) // Actual column type doesn't change
    def createdAt: Column[LocalDateTime] = column[LocalDateTime]("created_at", TIMESTAMP, NOT_NULL, DEFAULT_CURRENT_TIMESTAMP)
    
    override def * : Column[Product] = (id *: name *: price *: description *: categoryId *: createdAt).to[Product]
}

Detailed YAML Customization Syntax

In the customization YAML file, the following configurations are possible:

database:
  name: '{database_name}'
  tables:
    - name: '{table_name}'
      columns: # Optional
        - name: '{column_name}'
          type: '{desired_Scala_type}'
      class: # Optional
        extends:
          - '{package_path_of_trait_to_extend_the_model_class}'
      object: # Optional
        extends:
          - '{package_path_of_trait_to_extend_the_object}'

Example: Adding Traits to a Model Class

database:
  name: 'shop'
  tables:
    - name: 'products'
      class:
        extends:
          - 'com.example.JsonSerializable'
          - 'com.example.Validatable'

Example: Customizing Multiple Tables and Columns

database:
  name: 'shop'
  tables:
    - name: 'products'
      columns:
        - name: 'price'
          type: 'Money'
      object:
        extends:
          - 'com.example.MoneyTypeMapping'
    - name: 'orders'
      columns:
        - name: 'status'
          type: 'OrderStatus'
      object:
        extends:
          - 'com.example.OrderStatusMapping'

Using Generated Code

Generated code can be used like any other ldbc code.

import ldbc.dsl.*
import ldbc.generated.shop.Product

val provider = MySQLConnectionProvider(...)

// Referencing table queries
val products = Product.table

// Executing queries
val allProducts = provider.use { conn =>
  products.filter(_.price > 100).all.run(conn)
}

Best Practices for Code Generation

1. Clear SQL File Structure

2. Consistent Naming Conventions

3. Smart Use of Custom Types

4. Automate Regeneration

Consider integrating into CI/CD pipelines for regular schema updates.

Troubleshooting

When Code Is Not Generated

When Type Conversion Errors Occur

When Generated Code Has Issues

Tutorial Completion

Congratulations! You've completed all sections of the ldbc tutorial. Now you have the basic skills and knowledge to develop database applications using ldbc.

Throughout this journey, you've learned:

Use this knowledge to build type-safe and efficient database applications. For more information and updates, refer to the official documentation and GitHub repository.

Happy coding with ldbc!