Skip to content

Commit

Permalink
open enums
Browse files Browse the repository at this point in the history
  • Loading branch information
oyvindberg committed Aug 16, 2024
1 parent 346eda2 commit 0410bce
Show file tree
Hide file tree
Showing 96 changed files with 3,289 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object Number {
val Names: String = All.map(_.value).mkString(", ")
val ByName: Map[String, Number] = All.map(x => (x.value, x)).toMap

implicit lazy val arrayColumn: Column[Array[Number]] = Column.columnToArray(column, implicitly)
implicit lazy val arrayColumn: Column[Array[Number]] = Column.columnToArray[String](Column.columnToString, implicitly).map(_.map(Number.force))
implicit lazy val arrayToStatement: ToStatement[Array[Number]] = ToStatement[Array[Number]]((ps, i, arr) => ps.setArray(i, ps.getConnection.createArrayOf("myschema.number", arr.map[AnyRef](_.value))))
implicit lazy val column: Column[Number] = Column.columnToString.mapResult(str => Number(str).left.map(SqlMappingError.apply))
implicit lazy val ordering: Ordering[Number] = Ordering.by(_.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object Sector {
val Names: String = All.map(_.value).mkString(", ")
val ByName: Map[String, Sector] = All.map(x => (x.value, x)).toMap

implicit lazy val arrayColumn: Column[Array[Sector]] = Column.columnToArray(column, implicitly)
implicit lazy val arrayColumn: Column[Array[Sector]] = Column.columnToArray[String](Column.columnToString, implicitly).map(_.map(Sector.force))
implicit lazy val arrayToStatement: ToStatement[Array[Sector]] = ToStatement[Array[Sector]]((ps, i, arr) => ps.setArray(i, ps.getConnection.createArrayOf("myschema.sector", arr.map[AnyRef](_.value))))
implicit lazy val column: Column[Sector] = Column.columnToString.mapResult(str => Sector(str).left.map(SqlMappingError.apply))
implicit lazy val ordering: Ordering[Sector] = Ordering.by(_.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object Number {
val Names: String = All.map(_.value).mkString(", ")
val ByName: Map[String, Number] = All.map(x => (x.value, x)).toMap

implicit lazy val arrayColumn: Column[Array[Number]] = Column.columnToArray(column, implicitly)
implicit lazy val arrayColumn: Column[Array[Number]] = Column.columnToArray[String](Column.columnToString, implicitly).map(_.map(Number.force))
implicit lazy val arrayToStatement: ToStatement[Array[Number]] = ToStatement[Array[Number]]((ps, i, arr) => ps.setArray(i, ps.getConnection.createArrayOf("myschema.number", arr.map[AnyRef](_.value))))
implicit lazy val column: Column[Number] = Column.columnToString.mapResult(str => Number(str).left.map(SqlMappingError.apply))
implicit lazy val ordering: Ordering[Number] = Ordering.by(_.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object Sector {
val Names: String = All.map(_.value).mkString(", ")
val ByName: Map[String, Sector] = All.map(x => (x.value, x)).toMap

implicit lazy val arrayColumn: Column[Array[Sector]] = Column.columnToArray(column, implicitly)
implicit lazy val arrayColumn: Column[Array[Sector]] = Column.columnToArray[String](Column.columnToString, implicitly).map(_.map(Sector.force))
implicit lazy val arrayToStatement: ToStatement[Array[Sector]] = ToStatement[Array[Sector]]((ps, i, arr) => ps.setArray(i, ps.getConnection.createArrayOf("myschema.sector", arr.map[AnyRef](_.value))))
implicit lazy val column: Column[Sector] = Column.columnToString.mapResult(str => Sector(str).left.map(SqlMappingError.apply))
implicit lazy val ordering: Ordering[Sector] = Ordering.by(_.value)
Expand Down
2 changes: 1 addition & 1 deletion bleep.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$schema: https://raw.githubusercontent.com/oyvindberg/bleep/master/schema.json
$version: 0.0.4
$version: 0.0.6
jvm:
name: graalvm-community:21.0.2
projects:
Expand Down
11 changes: 11 additions & 0 deletions init/data/test-tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,15 @@ create table flaff
constraint flaff_parent_fk foreign key (code, another_code, some_number, parentSpecifier) references flaff
);

create table title (code text primary key);
insert into title (code) values ('mr'), ('ms'), ('dr'), ('phd');

create table title_domain (code short_text primary key);
insert into title_domain (code) values ('mr'), ('ms'), ('dr'), ('phd');

create table titledperson
(
title_short short_text not null references title_domain,
title text not null references title,
name text not null
);
7 changes: 4 additions & 3 deletions site-in/customization/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ val options = Options(
| `nullabilityOverride` | Defines nullability overrides for specific columns See section below. |
| `generateMockRepos` | Specifies which repositories to generate mock versions for (default is all). |
| `enableFieldValue` | Controls whether to enable `FieldValue` code generation for specific repositories (default is disabled). |
| `enableStreamingInserts` | Controls whether to enable [streaming inserts](../other-features/streaming-inserts.md) |
| `enableTestInserts` | Controls whether to enable [test inserts](../other-features/testing-with-random-values.md) for specific repositories (default is none). |
| `enablePrimaryKeyType` | Controls whether to enable [primary key types](../type-safety/id-types.md) for specific repositories (default is all). |
| `enableStreamingInserts` | Controls whether to enable [streaming inserts](../other-features/streaming-inserts.md) |
| `enableTestInserts` | Controls whether to enable [test inserts](../other-features/testing-with-random-values.md) for specific repositories (default is none). |
| `enablePrimaryKeyType` | Controls whether to enable [primary key types](../type-safety/id-types.md) for specific repositories (default is all). |
| `readonlyRepo` | Specifies whether to generate read-only repositories for specific repositories. Useful when you're working on a part of the system where you only consume certain tables. (default is `false` - all mutable). |
| `enableDsl` | Enables the [SQL DSL](../what-is/dsl.md) for code generation (default is `false`). |
| `keepDependencies` | Specifies whether to generate [table dependencies](../type-safety/type-flow.md) in generated code even if you didn't select them (default is `false`). |
| `rewriteDatabase` | Let's you perform arbitrary rewrites of database schema snapshot. you can add/remove rows, foreign keys and so on. |
| `openEnums` | Controls if you want to tag tables ids as [open string enums](../type-safety/open-string-enums.md) |

## Development options

Expand Down
59 changes: 59 additions & 0 deletions site-in/type-safety/open-string-enums.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Open string enums
---

Some people like to use tables and foreign keys to encode enums. It'll typically take this form:

```sql
create table title (code text primary key);
insert into title (code) values ('mr'), ('ms'), ('dr'), ('phd');

create table titledperson
(
title text not null references title,
name text not null
);

```

You can configure typo to generate so-called "open enums" for you.

```scala
val options = Options(
// ...
openEnums = Selector.relationNames("title")
)
```


And typo will output the [Primary key type](./id-types.md) as something like this:

```scala
/** Type for the primary key of table `public.title`. It has some known values:
* - dr
* - mr
* - ms
* - phd
*/
sealed abstract class TitleId(val value: String)

object TitleId {
def apply(underlying: String): TitleId =
ByName.getOrElse(underlying, Unknown(underlying))
case object dr extends TitleId("dr")
case object mr extends TitleId("mr")
case object ms extends TitleId("ms")
case object phd extends TitleId("phd")
case class Unknown(override val value: String) extends TitleId(value)
val All: List[TitleId] = List(dr, mr, ms, phd)
val ByName: Map[String, TitleId] = All.map(x => (x.value, x)).toMap

// type class instances
}
```

## Supported data types

Currently, typo supports the following data types for open enums:
- `text`
- a domain which has `text` as its base type
1 change: 1 addition & 0 deletions site/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const sidebars = {
items: [
{type: "doc", id: "type-safety/id-types"},
{type: "doc", id: "type-safety/string-enums"},
{type: "doc", id: "type-safety/open-string-enums"},
{type: "doc", id: "type-safety/domains"},
{type: "doc", id: "type-safety/arrays"},
{type: "doc", id: "type-safety/date-time"},
Expand Down
3 changes: 2 additions & 1 deletion typo-scripts/src/scala/scripts/CompileBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ object CompileBenchmark extends BleepScript("CompileBenchmark") {
Selector.ExcludePostgresInternal, // All
sqlFiles,
Nil
)
),
Map.empty
).foreach(_.overwriteFolder())

crossIds.map { crossId =>
Expand Down
15 changes: 7 additions & 8 deletions typo-scripts/src/scala/scripts/GenHardcodedFiles.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import typo.internal.{DebugJson, Lazy, generate}
// this runs automatically at build time to instantly see results.
// it does not need a running database
object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
val enums = List(
db.StringEnum(db.RelationName(Some("myschema"), "sector"), List("PUBLIC", "PRIVATE", "OTHER")),
db.StringEnum(db.RelationName(Some("myschema"), "number"), List("one", "two", "three"))
)
val sector = db.StringEnum(db.RelationName(Some("myschema"), "sector"), NonEmptyList("PUBLIC", "PRIVATE", "OTHER"))
val number = db.StringEnum(db.RelationName(Some("myschema"), "number"), NonEmptyList("one", "two", "three"))

val person = db.Table(
name = db.RelationName(Some("myschema"), "person"),
Expand Down Expand Up @@ -51,7 +49,7 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
db.Col(ParsedName.of("work_email"), db.Type.VarChar(Some(254)), Some("varchar"), Nullability.Nullable, columnDefault = None, None, None, Nil, DebugJson.Empty),
db.Col(
parsedName = ParsedName.of("sector"),
tpe = db.Type.EnumRef(db.RelationName(Some("myschema"), "sector")),
tpe = db.Type.EnumRef(sector),
udtName = Some("myschema.sector"),
nullability = Nullability.NoNulls,
columnDefault = Some("PUBLIC"),
Expand All @@ -62,7 +60,7 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
),
db.Col(
parsedName = ParsedName.of("favorite_number"),
tpe = db.Type.EnumRef(db.RelationName(Some("myschema"), "number")),
tpe = db.Type.EnumRef(number),
udtName = Some("myschema.number"),
nullability = Nullability.NoNulls,
columnDefault = Some("one"),
Expand Down Expand Up @@ -144,7 +142,7 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
else (DbLibName.Anorm, JsonLibName.PlayJson)
val domains = Nil

val metaDb = MetaDb(relations = all.map(t => t.name -> Lazy(t)).toMap, enums = enums, domains = domains)
val metaDb = MetaDb(relations = all.map(t => t.name -> Lazy(t)).toMap, enums = List(sector, number), domains = domains)

val generated: List[Generated] =
generate(
Expand All @@ -163,7 +161,8 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
silentBanner = true
),
metaDb,
ProjectGraph(name = "", target.sources, None, Selector.All, scripts = Nil, Nil)
ProjectGraph(name = "", target.sources, None, Selector.All, scripts = Nil, Nil),
Map.empty
)

generated.foreach(
Expand Down
20 changes: 16 additions & 4 deletions typo-scripts/src/scala/scripts/GeneratedAdventureWorks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package scripts
import bleep.logging.{Formatter, LogLevel, Loggers}
import bleep.{FileWatching, LogPatterns, cli}
import typo.*
import typo.internal.metadb.OpenEnum
import typo.internal.sqlfiles.readSqlFileDirectories
import typo.internal.{FileSync, generate}

Expand All @@ -27,16 +28,26 @@ object GeneratedAdventureWorks {
val ds = TypoDataSource.hikari(server = "localhost", port = 6432, databaseName = "Adventureworks", username = "postgres", password = "password")
val scriptsPath = buildDir.resolve("adventureworks_sql")
val selector = Selector.ExcludePostgresInternal
val metadb = Await.result(MetaDb.fromDb(TypoLogger.Console, ds, selector, schemaMode = SchemaMode.MultiSchema), Duration.Inf)

val typoLogger = TypoLogger.Console
val metadb = Await.result(MetaDb.fromDb(typoLogger, ds, selector, schemaMode = SchemaMode.MultiSchema), Duration.Inf)
val relationNameToOpenEnum = Await.result(
OpenEnum.find(
ds,
typoLogger,
Selector.All,
openEnumSelector = Selector.relationNames("title", "title_domain"),
metaDb = metadb
),
Duration.Inf
)
val variants = List(
(DbLibName.Anorm, JsonLibName.PlayJson, "typo-tester-anorm", new AtomicReference(Map.empty[RelPath, sc.Code])),
(DbLibName.Doobie, JsonLibName.Circe, "typo-tester-doobie", new AtomicReference(Map.empty[RelPath, sc.Code])),
(DbLibName.ZioJdbc, JsonLibName.ZioJson, "typo-tester-zio-jdbc", new AtomicReference(Map.empty[RelPath, sc.Code]))
)

def go(): Unit = {
val newSqlScripts = Await.result(readSqlFileDirectories(TypoLogger.Console, scriptsPath, ds), Duration.Inf)
val newSqlScripts = Await.result(readSqlFileDirectories(typoLogger, scriptsPath, ds), Duration.Inf)

variants.foreach { case (dbLib, jsonLib, projectPath, oldFilesRef) =>
val options = Options(
Expand All @@ -47,6 +58,7 @@ object GeneratedAdventureWorks {
case (_, "firstname") => "adventureworks.userdefined.FirstName"
case ("sales.creditcard", "creditcardid") => "adventureworks.userdefined.CustomCreditcardId"
},
openEnums = Selector.relationNames("title", "title_domain"),
generateMockRepos = !Selector.relationNames("purchaseorderdetail"),
enablePrimaryKeyType = !Selector.relationNames("billofmaterials"),
enableTestInserts = Selector.All,
Expand All @@ -56,7 +68,7 @@ object GeneratedAdventureWorks {
val targetSources = buildDir.resolve(s"$projectPath/generated-and-checked-in")

val newFiles: Generated =
generate(options, metadb, ProjectGraph(name = "", targetSources, None, selector, newSqlScripts, Nil)).head
generate(options, metadb, ProjectGraph(name = "", targetSources, None, selector, newSqlScripts, Nil), relationNameToOpenEnum).head

val knownUnchanged: Set[RelPath] = {
val oldFiles = oldFilesRef.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ object Myenum {
val Names: String = All.map(_.value).mkString(", ")
val ByName: Map[String, Myenum] = All.map(x => (x.value, x)).toMap

implicit lazy val arrayColumn: Column[Array[Myenum]] = Column.columnToArray(column, implicitly)
implicit lazy val arrayColumn: Column[Array[Myenum]] = Column.columnToArray[String](Column.columnToString, implicitly).map(_.map(Myenum.force))
implicit lazy val arrayToStatement: ToStatement[Array[Myenum]] = ToStatement[Array[Myenum]]((ps, i, arr) => ps.setArray(i, ps.getConnection.createArrayOf("public.myenum", arr.map[AnyRef](_.value))))
implicit lazy val column: Column[Myenum] = Column.columnToString.mapResult(str => Myenum(str).left.map(SqlMappingError.apply))
implicit lazy val ordering: Ordering[Myenum] = Ordering.by(_.value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* File has been automatically generated by `typo`.
*
* IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN.
*/
package adventureworks
package public
package title

import typo.dsl.Path
import typo.dsl.SqlExpr.FieldLikeNoHkt
import typo.dsl.SqlExpr.IdField
import typo.dsl.Structure.Relation

trait TitleFields {
def code: IdField[TitleId, TitleRow]
}

object TitleFields {
lazy val structure: Relation[TitleFields, TitleRow] =
new Impl(Nil)

private final class Impl(val _path: List[Path])
extends Relation[TitleFields, TitleRow] {

override lazy val fields: TitleFields = new TitleFields {
override def code = IdField[TitleId, TitleRow](_path, "code", None, None, x => x.code, (row, value) => row.copy(code = value))
}

override lazy val columns: List[FieldLikeNoHkt[?, TitleRow]] =
List[FieldLikeNoHkt[?, TitleRow]](fields.code)

override def copy(path: List[Path]): Impl =
new Impl(path)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* File has been automatically generated by `typo`.
*
* IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN.
*/
package adventureworks
package public
package title

import anorm.Column
import anorm.ParameterMetaData
import anorm.ToStatement
import java.sql.Types
import play.api.libs.json.JsValue
import play.api.libs.json.Reads
import play.api.libs.json.Writes

/** Type for the primary key of table `public.title`. It has some known values:
* - dr
* - mr
* - ms
* - phd
*/
sealed abstract class TitleId(val value: String)

object TitleId {
def apply(underlying: String): TitleId =
ByName.getOrElse(underlying, Unknown(underlying))
case object dr extends TitleId("dr")
case object mr extends TitleId("mr")
case object ms extends TitleId("ms")
case object phd extends TitleId("phd")
case class Unknown(override val value: String) extends TitleId(value)
val All: List[TitleId] = List(dr, mr, ms, phd)
val ByName: Map[String, TitleId] = All.map(x => (x.value, x)).toMap

implicit lazy val arrayColumn: Column[Array[TitleId]] = Column.columnToArray[String](Column.columnToString, implicitly).map(_.map(TitleId.apply))
implicit lazy val arrayToStatement: ToStatement[Array[TitleId]] = ToStatement.arrayToParameter(ParameterMetaData.StringParameterMetaData).contramap(_.map(_.value))
implicit lazy val column: Column[TitleId] = Column.columnToString.map(TitleId.apply)
implicit lazy val ordering: Ordering[TitleId] = Ordering.by(_.value)
implicit lazy val parameterMetadata: ParameterMetaData[TitleId] = new ParameterMetaData[TitleId] {
override def sqlType: String = "text"
override def jdbcType: Int = Types.OTHER
}
implicit lazy val reads: Reads[TitleId] = Reads[TitleId]{(value: JsValue) => value.validate(Reads.StringReads).map(TitleId.apply)}
implicit lazy val text: Text[TitleId] = new Text[TitleId] {
override def unsafeEncode(v: TitleId, sb: StringBuilder) = Text.stringInstance.unsafeEncode(v.value, sb)
override def unsafeArrayEncode(v: TitleId, sb: StringBuilder) = Text.stringInstance.unsafeArrayEncode(v.value, sb)
}
implicit lazy val toStatement: ToStatement[TitleId] = ToStatement.stringToStatement.contramap(_.value)
implicit lazy val writes: Writes[TitleId] = Writes[TitleId](value => Writes.StringWrites.writes(value.value))
}
Loading

0 comments on commit 0410bce

Please sign in to comment.