Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/2024 10 renewal schema #329

Merged
merged 186 commits into from
Dec 7, 2024
Merged

Conversation

takapi327
Copy link
Owner

@takapi327 takapi327 commented Nov 25, 2024

Implementation Details

This pull request includes significant functionality improvements and enhancements. Please see below for details.

Enhancement

Adding Column Annotation

Table generation using Deriving used the property name as it is as the column name.

case class City(
  id:          Int,
  name:        String,
  countryCode: String,
  district:    String,
  population:  Int
) derives Table

The formatting of the column names could be adjusted by implicitly passing Naming.

given Naming = Naming.PASCAL

However, this alone could not address all cases. For example, the following column names cannot be used in the previous implementation.

CREATE TABLE city (
  ID Bigint
  CountryID Bigint
)

Naming supports only certain formats (CAMEL, PASCAL, and SNAKE cases) and does not support column names that are all uppercase or only uppercase in certain parts of the column name.

Such cases can be resolved by using column annotations.

case class City(
  @Column("ID") id: Int,
  name:             String,
  countryCode:      String,
  district:         String,
  population:       Int
) derives Table

The overall formatting is done by Naming, and changes to only specific columns can be made using this annotation, which allows for flexible customization of column names.

Support for INSERT SELECT/JOIN

There are two ways to pass values in an INSERT statement: by VALUES or by using a SELECT statement or JOIN.

This modification supports both syntaxes.

TableQuery[City]
   .insertInto(city => city. id *: city. name)
   .select(
     TableQuery[Country]
       .select(country => country. id *: city. name)
   )

The same can be defined for using the records resulting from a JOIN.

TableQuery[City]
   .insertInto(city => city. id *: city. name)
   .select(
     TableQuery[Country]
       .join(TableQuery[City])
       .on((country, city) => country. id === city. countryId)
       .select((country, city) => country. id *: city. name)
       .where((country, city) => city. population > 1000000)
   )

Breaking Change

Change column refinement method

So far, column refinement has simply grouped the columns used as Tuple.

cityTable.select(city => (city.id, city.name))

However, there was a problem with this. Column is a type with a single type parameter. Until Scala2, there was a limit to the number of Tuples that could be passed. Scala 2 had a limit on the number of Tuples, so it was necessary to create a boilerplate or something that could handle all the number of Tuples.
In this case, dynamic Tuples are treated as Tuples or Tuple.Map, so if you wanted to access a Column type, you had to cast the type using asInstanceOf because its type can only be treated as a Tuple.
Casting the type would of course lose its type safety and complicate the code.

We decided to adopt twiddles, one of the same TypeLevel projects, to solve this problem.

Columns can be more easily composited by using twiddles.

cityTable.select(city => city.id *: city.name)

Composition itself is implementor-friendly because it can be done using *:, and this eliminates the need for unsafe typecasting since the internal code can just use Column[T] instead of Tuple.

Twiddles also make it easy to convert the composite result to another type.

case class City(id: Long, name: String)

def id: Column[Int] = column[Int]("ID")
def name: Column[String] = column[String]("Name")

def city: Column[City] = (id *: name).to[City]

Change from Table to TableQuery

Until now, the same Table type has been used to construct queries using the Table type and the Table type with Table information from the model.

case class City(id: Long, name: String) derives Table
val cityTable = Table[City]

Because the same type was used to represent two things, it was easy to implement incorrectly.

cityTable.select(city => city.insert(???))

Development tools such as IDEs could cause no small amount of confusion to implementers because they complement all available APIs.

case class City(id: Long, name: String) derives Table
val cityTable = TableQuery[City]

To solve this problem, we created a new type called TableQuery and separated the implementation of query construction.

Changes to Insert

The Insert statement is similarly modified so that columns are composited and inserted columns are specified.

cityTable
- .insertInto(city => (city.id, city.name))
+ .insertInto(city => city.id *: city.name)
  .values((1L, "Tokyo"))

Changes to Update

Previously, the Update Statement had to be set up one by one for each column to be updated. This implementation is convenient when you want to update a few columns individually, but it is very cumbersome to write set for each additional column you want to update.

cityTable
  .update("id", 1L)
  .set("name", "Tokyo")
  .set("population", 1, false)

With this update, columns can now be combined, allowing multiple columns to be specified together for update processing.

cityTable
  .update(city => city.id *: city.name)((1L, "Tokyo"))
  .set(_.population, 1, false)

With this update, columns can now be combined, allowing multiple columns to be specified together for update processing. It is still possible to update only specific columns using set. In addition, since set can be used to set conditions for updating, it is possible to create a query that updates additional columns only when the conditions are positive.

Changes to Join

Previously, table joins were constructed by setting the join condition as the second argument.

cityTable.join(countryTable)((c, co) => c.countryCode === co.code)

From this change, the conditions for joining tables must be set via the on API. This change is the result of an internal implementation change.

cityTable.join(countryTable).on((c, co) => c.countryCode === co.code)

Pull Request Checklist

  • Wrote unit and integration tests
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code formatting by scalafmt (sbt scalafmtAll command execution)
  • Add copyright headers to new files

References

@takapi327 takapi327 merged commit 5d0200c into master Dec 7, 2024
27 checks passed
@takapi327 takapi327 deleted the refactor/2024-10-Renewal-Schema branch December 7, 2024 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💪 enhancement Functional enhancement project:benchmark Addition and modification of functionality to Benchmark projects project:codegen Addition and modification of functionality to CodeGen projects project:queryBuilder Addition and modification of functionality to Query Builder projects project:schema Addition and modification of functionality to Schema projects 🔧 refactor Refactoring
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant