From 0410bcea4bc58535c5796e0f303c95ff2e2a97ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Raddum=20Berg?= Date: Thu, 6 Jun 2024 22:56:18 +0200 Subject: [PATCH] open enums --- .../testdb/hardcoded/myschema/Number.scala | 2 +- .../testdb/hardcoded/myschema/Sector.scala | 2 +- .../testdb/hardcoded/myschema/Number.scala | 2 +- .../testdb/hardcoded/myschema/Sector.scala | 2 +- bleep.yaml | 2 +- init/data/test-tables.sql | 11 ++ site-in/customization/overview.md | 7 +- site-in/type-safety/open-string-enums.md | 59 +++++++++ site/sidebars.js | 1 + .../src/scala/scripts/CompileBenchmark.scala | 3 +- .../src/scala/scripts/GenHardcodedFiles.scala | 15 ++- .../scripts/GeneratedAdventureWorks.scala | 20 ++- .../adventureworks/public/Myenum.scala | 2 +- .../public/title/TitleFields.scala | 37 ++++++ .../adventureworks/public/title/TitleId.scala | 52 ++++++++ .../public/title/TitleRepo.scala | 31 +++++ .../public/title/TitleRepoImpl.scala | 118 ++++++++++++++++++ .../public/title/TitleRepoMock.scala | 82 ++++++++++++ .../public/title/TitleRow.scala | 52 ++++++++ .../title_domain/TitleDomainFields.scala | 37 ++++++ .../public/title_domain/TitleDomainId.scala | 53 ++++++++ .../public/title_domain/TitleDomainRepo.scala | 31 +++++ .../title_domain/TitleDomainRepoImpl.scala | 118 ++++++++++++++++++ .../title_domain/TitleDomainRepoMock.scala | 82 ++++++++++++ .../public/title_domain/TitleDomainRow.scala | 52 ++++++++ .../titledperson/TitledpersonFields.scala | 54 ++++++++ .../titledperson/TitledpersonRepo.scala | 22 ++++ .../titledperson/TitledpersonRepoImpl.scala | 47 +++++++ .../public/titledperson/TitledpersonRow.scala | 67 ++++++++++ .../adventureworks/testInsert.scala | 14 +++ .../src/scala/adventureworks/ArrayTest.scala | 9 +- .../src/scala/adventureworks/JsonEquals.scala | 13 ++ .../scala/adventureworks/OpenEnumTest.scala | 35 ++++++ .../public/title/TitleFields.scala | 37 ++++++ .../adventureworks/public/title/TitleId.scala | 51 ++++++++ .../public/title/TitleRepo.scala | 32 +++++ .../public/title/TitleRepoImpl.scala | 96 ++++++++++++++ .../public/title/TitleRepoMock.scala | 100 +++++++++++++++ .../public/title/TitleRow.scala | 50 ++++++++ .../title_domain/TitleDomainFields.scala | 37 ++++++ .../public/title_domain/TitleDomainId.scala | 51 ++++++++ .../public/title_domain/TitleDomainRepo.scala | 32 +++++ .../title_domain/TitleDomainRepoImpl.scala | 96 ++++++++++++++ .../title_domain/TitleDomainRepoMock.scala | 100 +++++++++++++++ .../public/title_domain/TitleDomainRow.scala | 50 ++++++++ .../titledperson/TitledpersonFields.scala | 54 ++++++++ .../titledperson/TitledpersonRepo.scala | 23 ++++ .../titledperson/TitledpersonRepoImpl.scala | 46 +++++++ .../public/titledperson/TitledpersonRow.scala | 68 ++++++++++ .../adventureworks/testInsert.scala | 14 +++ .../src/scala/adventureworks/ArrayTest.scala | 9 +- .../src/scala/adventureworks/JsonEquals.scala | 12 ++ .../scala/adventureworks/OpenEnumTest.scala | 35 ++++++ .../public/title/TitleFields.scala | 37 ++++++ .../adventureworks/public/title/TitleId.scala | 51 ++++++++ .../public/title/TitleRepo.scala | 34 +++++ .../public/title/TitleRepoImpl.scala | 82 ++++++++++++ .../public/title/TitleRepoMock.scala | 91 ++++++++++++++ .../public/title/TitleRow.scala | 43 +++++++ .../title_domain/TitleDomainFields.scala | 37 ++++++ .../public/title_domain/TitleDomainId.scala | 52 ++++++++ .../public/title_domain/TitleDomainRepo.scala | 34 +++++ .../title_domain/TitleDomainRepoImpl.scala | 82 ++++++++++++ .../title_domain/TitleDomainRepoMock.scala | 91 ++++++++++++++ .../public/title_domain/TitleDomainRow.scala | 43 +++++++ .../titledperson/TitledpersonFields.scala | 54 ++++++++ .../titledperson/TitledpersonRepo.scala | 24 ++++ .../titledperson/TitledpersonRepoImpl.scala | 45 +++++++ .../public/titledperson/TitledpersonRow.scala | 67 ++++++++++ .../adventureworks/testInsert.scala | 14 +++ .../src/scala/adventureworks/ArrayTest.scala | 9 +- .../src/scala/adventureworks/JsonEquals.scala | 12 ++ .../scala/adventureworks/OpenEnumTest.scala | 35 ++++++ typo/src/scala/typo/Options.scala | 3 +- typo/src/scala/typo/db.scala | 4 +- typo/src/scala/typo/generateFromDb.scala | 6 +- .../typo/internal/ComputedStringEnum.scala | 11 +- .../scala/typo/internal/ComputedTable.scala | 42 ++++--- .../typo/internal/ComputedTestInserts.scala | 11 +- typo/src/scala/typo/internal/IdComputed.scala | 8 ++ .../scala/typo/internal/TypeMapperDb.scala | 2 +- .../scala/typo/internal/TypeMapperScala.scala | 2 +- .../scala/typo/internal/codegen/DbLib.scala | 2 +- .../typo/internal/codegen/DbLibAnorm.scala | 16 ++- .../typo/internal/codegen/DbLibDoobie.scala | 24 ++-- .../typo/internal/codegen/DbLibZioJdbc.scala | 44 ++++--- .../internal/codegen/FileStringEnum.scala | 6 +- .../typo/internal/codegen/FilesTable.scala | 67 ++++++++++ .../scala/typo/internal/codegen/JsonLib.scala | 2 +- .../typo/internal/codegen/JsonLibCirce.scala | 6 +- .../typo/internal/codegen/JsonLibPlay.scala | 9 +- .../internal/codegen/JsonLibZioJson.scala | 6 +- .../scala/typo/internal/codegen/SqlCast.scala | 2 +- typo/src/scala/typo/internal/generate.scala | 11 +- .../scala/typo/internal/metadb/Enums.scala | 6 +- .../scala/typo/internal/metadb/OpenEnum.scala | 58 +++++++++ 96 files changed, 3289 insertions(+), 123 deletions(-) create mode 100644 site-in/type-safety/open-string-enums.md create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleFields.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleId.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRow.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala create mode 100644 typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala create mode 100644 typo-tester-anorm/src/scala/adventureworks/JsonEquals.scala create mode 100644 typo-tester-anorm/src/scala/adventureworks/OpenEnumTest.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleFields.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleId.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRow.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala create mode 100644 typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala create mode 100644 typo-tester-doobie/src/scala/adventureworks/JsonEquals.scala create mode 100644 typo-tester-doobie/src/scala/adventureworks/OpenEnumTest.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleFields.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleId.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRow.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala create mode 100644 typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala create mode 100644 typo-tester-zio-jdbc/src/scala/adventureworks/JsonEquals.scala create mode 100644 typo-tester-zio-jdbc/src/scala/adventureworks/OpenEnumTest.scala create mode 100644 typo/src/scala/typo/internal/metadb/OpenEnum.scala diff --git a/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala b/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala index 645b3b8bd6..a9e73340e8 100644 --- a/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala +++ b/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala @@ -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) diff --git a/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala b/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala index 6cb2e0396d..7d7be0dcd1 100644 --- a/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala +++ b/.bleep/generated-sources/typo-tester-anorm@jvm213/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala @@ -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) diff --git a/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala b/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala index 645b3b8bd6..a9e73340e8 100644 --- a/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala +++ b/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Number.scala @@ -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) diff --git a/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala b/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala index 6cb2e0396d..7d7be0dcd1 100644 --- a/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala +++ b/.bleep/generated-sources/typo-tester-anorm@jvm3/scripts.GenHardcodedFiles/testdb/hardcoded/myschema/Sector.scala @@ -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) diff --git a/bleep.yaml b/bleep.yaml index 918af0ae1d..94bec31ff4 100644 --- a/bleep.yaml +++ b/bleep.yaml @@ -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: diff --git a/init/data/test-tables.sql b/init/data/test-tables.sql index 08906bf1a2..cda710cd71 100644 --- a/init/data/test-tables.sql +++ b/init/data/test-tables.sql @@ -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 +); diff --git a/site-in/customization/overview.md b/site-in/customization/overview.md index f31d9d7d24..e9dcdf232b 100644 --- a/site-in/customization/overview.md +++ b/site-in/customization/overview.md @@ -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 diff --git a/site-in/type-safety/open-string-enums.md b/site-in/type-safety/open-string-enums.md new file mode 100644 index 0000000000..71352477bf --- /dev/null +++ b/site-in/type-safety/open-string-enums.md @@ -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 \ No newline at end of file diff --git a/site/sidebars.js b/site/sidebars.js index 705c5a011b..c509b725d7 100644 --- a/site/sidebars.js +++ b/site/sidebars.js @@ -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"}, diff --git a/typo-scripts/src/scala/scripts/CompileBenchmark.scala b/typo-scripts/src/scala/scripts/CompileBenchmark.scala index 70ca23e713..e8b25f87d3 100644 --- a/typo-scripts/src/scala/scripts/CompileBenchmark.scala +++ b/typo-scripts/src/scala/scripts/CompileBenchmark.scala @@ -72,7 +72,8 @@ object CompileBenchmark extends BleepScript("CompileBenchmark") { Selector.ExcludePostgresInternal, // All sqlFiles, Nil - ) + ), + Map.empty ).foreach(_.overwriteFolder()) crossIds.map { crossId => diff --git a/typo-scripts/src/scala/scripts/GenHardcodedFiles.scala b/typo-scripts/src/scala/scripts/GenHardcodedFiles.scala index 1b1054d382..7562a47d05 100644 --- a/typo-scripts/src/scala/scripts/GenHardcodedFiles.scala +++ b/typo-scripts/src/scala/scripts/GenHardcodedFiles.scala @@ -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"), @@ -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"), @@ -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"), @@ -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( @@ -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( diff --git a/typo-scripts/src/scala/scripts/GeneratedAdventureWorks.scala b/typo-scripts/src/scala/scripts/GeneratedAdventureWorks.scala index f68cfa9593..ae5a7dfe46 100644 --- a/typo-scripts/src/scala/scripts/GeneratedAdventureWorks.scala +++ b/typo-scripts/src/scala/scripts/GeneratedAdventureWorks.scala @@ -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} @@ -27,8 +28,18 @@ 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])), @@ -36,7 +47,7 @@ object GeneratedAdventureWorks { ) 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( @@ -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, @@ -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() diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/Myenum.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/Myenum.scala index ed72715fa6..63435e0231 100644 --- a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/Myenum.scala +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/Myenum.scala @@ -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) diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleFields.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleFields.scala new file mode 100644 index 0000000000..a8ed6e549d --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleFields.scala @@ -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) + } + +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleId.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleId.scala new file mode 100644 index 0000000000..bfe38d4e59 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleId.scala @@ -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)) +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala new file mode 100644 index 0000000000..77bb2365d5 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala @@ -0,0 +1,31 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import java.sql.Connection +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder + +trait TitleRepo { + def delete: DeleteBuilder[TitleFields, TitleRow] + def deleteById(code: TitleId)(implicit c: Connection): Boolean + def deleteByIds(codes: Array[TitleId])(implicit c: Connection): Int + def insert(unsaved: TitleRow)(implicit c: Connection): TitleRow + def insertStreaming(unsaved: Iterator[TitleRow], batchSize: Int = 10000)(implicit c: Connection): Long + def select: SelectBuilder[TitleFields, TitleRow] + def selectAll(implicit c: Connection): List[TitleRow] + def selectById(code: TitleId)(implicit c: Connection): Option[TitleRow] + def selectByIds(codes: Array[TitleId])(implicit c: Connection): List[TitleRow] + def selectByIdsTracked(codes: Array[TitleId])(implicit c: Connection): Map[TitleId, TitleRow] + def update: UpdateBuilder[TitleFields, TitleRow] + def upsert(unsaved: TitleRow)(implicit c: Connection): TitleRow + def upsertBatch(unsaved: Iterable[TitleRow])(implicit c: Connection): List[TitleRow] + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + def upsertStreaming(unsaved: Iterator[TitleRow], batchSize: Int = 10000)(implicit c: Connection): Int +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala new file mode 100644 index 0000000000..68200f8b97 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala @@ -0,0 +1,118 @@ +/** + * 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.BatchSql +import anorm.NamedParameter +import anorm.ParameterValue +import anorm.SqlStringInterpolation +import java.sql.Connection +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder + +class TitleRepoImpl extends TitleRepo { + override def delete: DeleteBuilder[TitleFields, TitleRow] = { + DeleteBuilder(""""public"."title"""", TitleFields.structure) + } + override def deleteById(code: TitleId)(implicit c: Connection): Boolean = { + SQL"""delete from "public"."title" where "code" = ${ParameterValue(code, null, TitleId.toStatement)}""".executeUpdate() > 0 + } + override def deleteByIds(codes: Array[TitleId])(implicit c: Connection): Int = { + SQL"""delete + from "public"."title" + where "code" = ANY(${codes}) + """.executeUpdate() + + } + override def insert(unsaved: TitleRow)(implicit c: Connection): TitleRow = { + SQL"""insert into "public"."title"("code") + values (${ParameterValue(unsaved.code, null, TitleId.toStatement)}) + returning "code" + """ + .executeInsert(TitleRow.rowParser(1).single) + + } + override def insertStreaming(unsaved: Iterator[TitleRow], batchSize: Int = 10000)(implicit c: Connection): Long = { + streamingInsert(s"""COPY "public"."title"("code") FROM STDIN""", batchSize, unsaved)(TitleRow.text, c) + } + override def select: SelectBuilder[TitleFields, TitleRow] = { + SelectBuilderSql(""""public"."title"""", TitleFields.structure, TitleRow.rowParser) + } + override def selectAll(implicit c: Connection): List[TitleRow] = { + SQL"""select "code" + from "public"."title" + """.as(TitleRow.rowParser(1).*) + } + override def selectById(code: TitleId)(implicit c: Connection): Option[TitleRow] = { + SQL"""select "code" + from "public"."title" + where "code" = ${ParameterValue(code, null, TitleId.toStatement)} + """.as(TitleRow.rowParser(1).singleOpt) + } + override def selectByIds(codes: Array[TitleId])(implicit c: Connection): List[TitleRow] = { + SQL"""select "code" + from "public"."title" + where "code" = ANY(${codes}) + """.as(TitleRow.rowParser(1).*) + + } + override def selectByIdsTracked(codes: Array[TitleId])(implicit c: Connection): Map[TitleId, TitleRow] = { + val byId = selectByIds(codes).view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + override def update: UpdateBuilder[TitleFields, TitleRow] = { + UpdateBuilder(""""public"."title"""", TitleFields.structure, TitleRow.rowParser) + } + override def upsert(unsaved: TitleRow)(implicit c: Connection): TitleRow = { + SQL"""insert into "public"."title"("code") + values ( + ${ParameterValue(unsaved.code, null, TitleId.toStatement)} + ) + on conflict ("code") + do nothing + returning "code" + """ + .executeInsert(TitleRow.rowParser(1).single) + + } + override def upsertBatch(unsaved: Iterable[TitleRow])(implicit c: Connection): List[TitleRow] = { + def toNamedParameter(row: TitleRow): List[NamedParameter] = List( + NamedParameter("code", ParameterValue(row.code, null, TitleId.toStatement)) + ) + unsaved.toList match { + case Nil => Nil + case head :: rest => + new anorm.adventureworks.ExecuteReturningSyntax.Ops( + BatchSql( + s"""insert into "public"."title"("code") + values ({code}) + on conflict ("code") + do nothing + returning "code" + """, + toNamedParameter(head), + rest.map(toNamedParameter)* + ) + ).executeReturning(TitleRow.rowParser(1).*) + } + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Iterator[TitleRow], batchSize: Int = 10000)(implicit c: Connection): Int = { + SQL"""create temporary table title_TEMP (like "public"."title") on commit drop""".execute(): @nowarn + streamingInsert(s"""copy title_TEMP("code") from stdin""", batchSize, unsaved)(TitleRow.text, c): @nowarn + SQL"""insert into "public"."title"("code") + select * from title_TEMP + on conflict ("code") + do nothing + ; + drop table title_TEMP;""".executeUpdate() + } +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala new file mode 100644 index 0000000000..b03ae9a162 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala @@ -0,0 +1,82 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import java.sql.Connection +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.DeleteBuilder.DeleteBuilderMock +import typo.dsl.DeleteParams +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderMock +import typo.dsl.SelectParams +import typo.dsl.UpdateBuilder +import typo.dsl.UpdateBuilder.UpdateBuilderMock +import typo.dsl.UpdateParams + +class TitleRepoMock(map: scala.collection.mutable.Map[TitleId, TitleRow] = scala.collection.mutable.Map.empty) extends TitleRepo { + override def delete: DeleteBuilder[TitleFields, TitleRow] = { + DeleteBuilderMock(DeleteParams.empty, TitleFields.structure, map) + } + override def deleteById(code: TitleId)(implicit c: Connection): Boolean = { + map.remove(code).isDefined + } + override def deleteByIds(codes: Array[TitleId])(implicit c: Connection): Int = { + codes.map(id => map.remove(id)).count(_.isDefined) + } + override def insert(unsaved: TitleRow)(implicit c: Connection): TitleRow = { + val _ = if (map.contains(unsaved.code)) + sys.error(s"id ${unsaved.code} already exists") + else + map.put(unsaved.code, unsaved) + + unsaved + } + override def insertStreaming(unsaved: Iterator[TitleRow], batchSize: Int = 10000)(implicit c: Connection): Long = { + unsaved.foreach { row => + map += (row.code -> row) + } + unsaved.size.toLong + } + override def select: SelectBuilder[TitleFields, TitleRow] = { + SelectBuilderMock(TitleFields.structure, () => map.values.toList, SelectParams.empty) + } + override def selectAll(implicit c: Connection): List[TitleRow] = { + map.values.toList + } + override def selectById(code: TitleId)(implicit c: Connection): Option[TitleRow] = { + map.get(code) + } + override def selectByIds(codes: Array[TitleId])(implicit c: Connection): List[TitleRow] = { + codes.flatMap(map.get).toList + } + override def selectByIdsTracked(codes: Array[TitleId])(implicit c: Connection): Map[TitleId, TitleRow] = { + val byId = selectByIds(codes).view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + override def update: UpdateBuilder[TitleFields, TitleRow] = { + UpdateBuilderMock(UpdateParams.empty, TitleFields.structure, map) + } + override def upsert(unsaved: TitleRow)(implicit c: Connection): TitleRow = { + map.put(unsaved.code, unsaved): @nowarn + unsaved + } + override def upsertBatch(unsaved: Iterable[TitleRow])(implicit c: Connection): List[TitleRow] = { + unsaved.map { row => + map += (row.code -> row) + row + }.toList + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Iterator[TitleRow], batchSize: Int = 10000)(implicit c: Connection): Int = { + unsaved.foreach { row => + map += (row.code -> row) + } + unsaved.size + } +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRow.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRow.scala new file mode 100644 index 0000000000..8217a5aa7c --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title/TitleRow.scala @@ -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.RowParser +import anorm.Success +import play.api.libs.json.JsObject +import play.api.libs.json.JsResult +import play.api.libs.json.JsValue +import play.api.libs.json.OWrites +import play.api.libs.json.Reads +import scala.collection.immutable.ListMap +import scala.util.Try + +/** Table: public.title + Primary key: code */ +case class TitleRow( + code: TitleId +){ + val id = code + } + +object TitleRow { + implicit lazy val reads: Reads[TitleRow] = Reads[TitleRow](json => JsResult.fromTry( + Try( + TitleRow( + code = json.\("code").as(TitleId.reads) + ) + ) + ), + ) + def rowParser(idx: Int): RowParser[TitleRow] = RowParser[TitleRow] { row => + Success( + TitleRow( + code = row(idx + 0)(TitleId.column) + ) + ) + } + implicit lazy val text: Text[TitleRow] = Text.instance[TitleRow]{ (row, sb) => + TitleId.text.unsafeEncode(row.code, sb) + } + implicit lazy val writes: OWrites[TitleRow] = OWrites[TitleRow](o => + new JsObject(ListMap[String, JsValue]( + "code" -> TitleId.writes.writes(o.code) + )) + ) +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala new file mode 100644 index 0000000000..c87f1bab4b --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala @@ -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_domain + +import typo.dsl.Path +import typo.dsl.SqlExpr.FieldLikeNoHkt +import typo.dsl.SqlExpr.IdField +import typo.dsl.Structure.Relation + +trait TitleDomainFields { + def code: IdField[TitleDomainId, TitleDomainRow] +} + +object TitleDomainFields { + lazy val structure: Relation[TitleDomainFields, TitleDomainRow] = + new Impl(Nil) + + private final class Impl(val _path: List[Path]) + extends Relation[TitleDomainFields, TitleDomainRow] { + + override lazy val fields: TitleDomainFields = new TitleDomainFields { + override def code = IdField[TitleDomainId, TitleDomainRow](_path, "code", None, Some("text"), x => x.code, (row, value) => row.copy(code = value)) + } + + override lazy val columns: List[FieldLikeNoHkt[?, TitleDomainRow]] = + List[FieldLikeNoHkt[?, TitleDomainRow]](fields.code) + + override def copy(path: List[Path]): Impl = + new Impl(path) + } + +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala new file mode 100644 index 0000000000..91110f6c33 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala @@ -0,0 +1,53 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +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_domain`. It has some known values: + * - dr + * - mr + * - ms + * - phd + */ +sealed abstract class TitleDomainId(val value: ShortText) + +object TitleDomainId { + def apply(underlying: ShortText): TitleDomainId = + ByName.getOrElse(underlying, Unknown(underlying)) + def shortText(value: String): TitleDomainId = TitleDomainId(ShortText(value)) + case object dr extends TitleDomainId(ShortText("dr")) + case object mr extends TitleDomainId(ShortText("mr")) + case object ms extends TitleDomainId(ShortText("ms")) + case object phd extends TitleDomainId(ShortText("phd")) + case class Unknown(override val value: ShortText) extends TitleDomainId(value) + val All: List[TitleDomainId] = List(dr, mr, ms, phd) + val ByName: Map[ShortText, TitleDomainId] = All.map(x => (x.value, x)).toMap + + implicit lazy val arrayColumn: Column[Array[TitleDomainId]] = ShortText.arrayColumn.map(_.map(TitleDomainId.apply)) + implicit lazy val arrayToStatement: ToStatement[Array[TitleDomainId]] = ShortText.arrayToStatement.contramap(_.map(_.value)) + implicit lazy val column: Column[TitleDomainId] = ShortText.column.map(TitleDomainId.apply) + implicit lazy val ordering: Ordering[TitleDomainId] = Ordering.by(_.value) + implicit lazy val parameterMetadata: ParameterMetaData[TitleDomainId] = new ParameterMetaData[TitleDomainId] { + override def sqlType: String = """"public"."short_text"""" + override def jdbcType: Int = Types.OTHER + } + implicit lazy val reads: Reads[TitleDomainId] = Reads[TitleDomainId]{(value: JsValue) => value.validate(ShortText.reads).map(TitleDomainId.apply)} + implicit lazy val text: Text[TitleDomainId] = new Text[TitleDomainId] { + override def unsafeEncode(v: TitleDomainId, sb: StringBuilder) = ShortText.text.unsafeEncode(v.value, sb) + override def unsafeArrayEncode(v: TitleDomainId, sb: StringBuilder) = ShortText.text.unsafeArrayEncode(v.value, sb) + } + implicit lazy val toStatement: ToStatement[TitleDomainId] = ShortText.toStatement.contramap(_.value) + implicit lazy val writes: Writes[TitleDomainId] = Writes[TitleDomainId](value => ShortText.writes.writes(value.value)) +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala new file mode 100644 index 0000000000..e79a7ed090 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala @@ -0,0 +1,31 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import java.sql.Connection +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder + +trait TitleDomainRepo { + def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] + def deleteById(code: TitleDomainId)(implicit c: Connection): Boolean + def deleteByIds(codes: Array[TitleDomainId])(implicit c: Connection): Int + def insert(unsaved: TitleDomainRow)(implicit c: Connection): TitleDomainRow + def insertStreaming(unsaved: Iterator[TitleDomainRow], batchSize: Int = 10000)(implicit c: Connection): Long + def select: SelectBuilder[TitleDomainFields, TitleDomainRow] + def selectAll(implicit c: Connection): List[TitleDomainRow] + def selectById(code: TitleDomainId)(implicit c: Connection): Option[TitleDomainRow] + def selectByIds(codes: Array[TitleDomainId])(implicit c: Connection): List[TitleDomainRow] + def selectByIdsTracked(codes: Array[TitleDomainId])(implicit c: Connection): Map[TitleDomainId, TitleDomainRow] + def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] + def upsert(unsaved: TitleDomainRow)(implicit c: Connection): TitleDomainRow + def upsertBatch(unsaved: Iterable[TitleDomainRow])(implicit c: Connection): List[TitleDomainRow] + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + def upsertStreaming(unsaved: Iterator[TitleDomainRow], batchSize: Int = 10000)(implicit c: Connection): Int +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala new file mode 100644 index 0000000000..4fb7f43cf9 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala @@ -0,0 +1,118 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import anorm.BatchSql +import anorm.NamedParameter +import anorm.ParameterValue +import anorm.SqlStringInterpolation +import java.sql.Connection +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder + +class TitleDomainRepoImpl extends TitleDomainRepo { + override def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] = { + DeleteBuilder(""""public"."title_domain"""", TitleDomainFields.structure) + } + override def deleteById(code: TitleDomainId)(implicit c: Connection): Boolean = { + SQL"""delete from "public"."title_domain" where "code" = ${ParameterValue(code, null, TitleDomainId.toStatement)}""".executeUpdate() > 0 + } + override def deleteByIds(codes: Array[TitleDomainId])(implicit c: Connection): Int = { + SQL"""delete + from "public"."title_domain" + where "code" = ANY(${codes}) + """.executeUpdate() + + } + override def insert(unsaved: TitleDomainRow)(implicit c: Connection): TitleDomainRow = { + SQL"""insert into "public"."title_domain"("code") + values (${ParameterValue(unsaved.code, null, TitleDomainId.toStatement)}::text) + returning "code" + """ + .executeInsert(TitleDomainRow.rowParser(1).single) + + } + override def insertStreaming(unsaved: Iterator[TitleDomainRow], batchSize: Int = 10000)(implicit c: Connection): Long = { + streamingInsert(s"""COPY "public"."title_domain"("code") FROM STDIN""", batchSize, unsaved)(TitleDomainRow.text, c) + } + override def select: SelectBuilder[TitleDomainFields, TitleDomainRow] = { + SelectBuilderSql(""""public"."title_domain"""", TitleDomainFields.structure, TitleDomainRow.rowParser) + } + override def selectAll(implicit c: Connection): List[TitleDomainRow] = { + SQL"""select "code" + from "public"."title_domain" + """.as(TitleDomainRow.rowParser(1).*) + } + override def selectById(code: TitleDomainId)(implicit c: Connection): Option[TitleDomainRow] = { + SQL"""select "code" + from "public"."title_domain" + where "code" = ${ParameterValue(code, null, TitleDomainId.toStatement)} + """.as(TitleDomainRow.rowParser(1).singleOpt) + } + override def selectByIds(codes: Array[TitleDomainId])(implicit c: Connection): List[TitleDomainRow] = { + SQL"""select "code" + from "public"."title_domain" + where "code" = ANY(${codes}) + """.as(TitleDomainRow.rowParser(1).*) + + } + override def selectByIdsTracked(codes: Array[TitleDomainId])(implicit c: Connection): Map[TitleDomainId, TitleDomainRow] = { + val byId = selectByIds(codes).view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + override def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] = { + UpdateBuilder(""""public"."title_domain"""", TitleDomainFields.structure, TitleDomainRow.rowParser) + } + override def upsert(unsaved: TitleDomainRow)(implicit c: Connection): TitleDomainRow = { + SQL"""insert into "public"."title_domain"("code") + values ( + ${ParameterValue(unsaved.code, null, TitleDomainId.toStatement)}::text + ) + on conflict ("code") + do nothing + returning "code" + """ + .executeInsert(TitleDomainRow.rowParser(1).single) + + } + override def upsertBatch(unsaved: Iterable[TitleDomainRow])(implicit c: Connection): List[TitleDomainRow] = { + def toNamedParameter(row: TitleDomainRow): List[NamedParameter] = List( + NamedParameter("code", ParameterValue(row.code, null, TitleDomainId.toStatement)) + ) + unsaved.toList match { + case Nil => Nil + case head :: rest => + new anorm.adventureworks.ExecuteReturningSyntax.Ops( + BatchSql( + s"""insert into "public"."title_domain"("code") + values ({code}::text) + on conflict ("code") + do nothing + returning "code" + """, + toNamedParameter(head), + rest.map(toNamedParameter)* + ) + ).executeReturning(TitleDomainRow.rowParser(1).*) + } + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Iterator[TitleDomainRow], batchSize: Int = 10000)(implicit c: Connection): Int = { + SQL"""create temporary table title_domain_TEMP (like "public"."title_domain") on commit drop""".execute(): @nowarn + streamingInsert(s"""copy title_domain_TEMP("code") from stdin""", batchSize, unsaved)(TitleDomainRow.text, c): @nowarn + SQL"""insert into "public"."title_domain"("code") + select * from title_domain_TEMP + on conflict ("code") + do nothing + ; + drop table title_domain_TEMP;""".executeUpdate() + } +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala new file mode 100644 index 0000000000..d16d22f591 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala @@ -0,0 +1,82 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import java.sql.Connection +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.DeleteBuilder.DeleteBuilderMock +import typo.dsl.DeleteParams +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderMock +import typo.dsl.SelectParams +import typo.dsl.UpdateBuilder +import typo.dsl.UpdateBuilder.UpdateBuilderMock +import typo.dsl.UpdateParams + +class TitleDomainRepoMock(map: scala.collection.mutable.Map[TitleDomainId, TitleDomainRow] = scala.collection.mutable.Map.empty) extends TitleDomainRepo { + override def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] = { + DeleteBuilderMock(DeleteParams.empty, TitleDomainFields.structure, map) + } + override def deleteById(code: TitleDomainId)(implicit c: Connection): Boolean = { + map.remove(code).isDefined + } + override def deleteByIds(codes: Array[TitleDomainId])(implicit c: Connection): Int = { + codes.map(id => map.remove(id)).count(_.isDefined) + } + override def insert(unsaved: TitleDomainRow)(implicit c: Connection): TitleDomainRow = { + val _ = if (map.contains(unsaved.code)) + sys.error(s"id ${unsaved.code} already exists") + else + map.put(unsaved.code, unsaved) + + unsaved + } + override def insertStreaming(unsaved: Iterator[TitleDomainRow], batchSize: Int = 10000)(implicit c: Connection): Long = { + unsaved.foreach { row => + map += (row.code -> row) + } + unsaved.size.toLong + } + override def select: SelectBuilder[TitleDomainFields, TitleDomainRow] = { + SelectBuilderMock(TitleDomainFields.structure, () => map.values.toList, SelectParams.empty) + } + override def selectAll(implicit c: Connection): List[TitleDomainRow] = { + map.values.toList + } + override def selectById(code: TitleDomainId)(implicit c: Connection): Option[TitleDomainRow] = { + map.get(code) + } + override def selectByIds(codes: Array[TitleDomainId])(implicit c: Connection): List[TitleDomainRow] = { + codes.flatMap(map.get).toList + } + override def selectByIdsTracked(codes: Array[TitleDomainId])(implicit c: Connection): Map[TitleDomainId, TitleDomainRow] = { + val byId = selectByIds(codes).view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + override def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] = { + UpdateBuilderMock(UpdateParams.empty, TitleDomainFields.structure, map) + } + override def upsert(unsaved: TitleDomainRow)(implicit c: Connection): TitleDomainRow = { + map.put(unsaved.code, unsaved): @nowarn + unsaved + } + override def upsertBatch(unsaved: Iterable[TitleDomainRow])(implicit c: Connection): List[TitleDomainRow] = { + unsaved.map { row => + map += (row.code -> row) + row + }.toList + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Iterator[TitleDomainRow], batchSize: Int = 10000)(implicit c: Connection): Int = { + unsaved.foreach { row => + map += (row.code -> row) + } + unsaved.size + } +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala new file mode 100644 index 0000000000..5cfa4cf48b --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala @@ -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_domain + +import anorm.RowParser +import anorm.Success +import play.api.libs.json.JsObject +import play.api.libs.json.JsResult +import play.api.libs.json.JsValue +import play.api.libs.json.OWrites +import play.api.libs.json.Reads +import scala.collection.immutable.ListMap +import scala.util.Try + +/** Table: public.title_domain + Primary key: code */ +case class TitleDomainRow( + code: TitleDomainId +){ + val id = code + } + +object TitleDomainRow { + implicit lazy val reads: Reads[TitleDomainRow] = Reads[TitleDomainRow](json => JsResult.fromTry( + Try( + TitleDomainRow( + code = json.\("code").as(TitleDomainId.reads) + ) + ) + ), + ) + def rowParser(idx: Int): RowParser[TitleDomainRow] = RowParser[TitleDomainRow] { row => + Success( + TitleDomainRow( + code = row(idx + 0)(TitleDomainId.column) + ) + ) + } + implicit lazy val text: Text[TitleDomainRow] = Text.instance[TitleDomainRow]{ (row, sb) => + TitleDomainId.text.unsafeEncode(row.code, sb) + } + implicit lazy val writes: OWrites[TitleDomainRow] = OWrites[TitleDomainRow](o => + new JsObject(ListMap[String, JsValue]( + "code" -> TitleDomainId.writes.writes(o.code) + )) + ) +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala new file mode 100644 index 0000000000..5b42488ef4 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala @@ -0,0 +1,54 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleFields +import adventureworks.public.title.TitleId +import adventureworks.public.title.TitleRow +import adventureworks.public.title_domain.TitleDomainFields +import adventureworks.public.title_domain.TitleDomainId +import adventureworks.public.title_domain.TitleDomainRow +import typo.dsl.ForeignKey +import typo.dsl.Path +import typo.dsl.SqlExpr.Field +import typo.dsl.SqlExpr.FieldLikeNoHkt +import typo.dsl.Structure.Relation + +trait TitledpersonFields { + def titleShort: Field[TitleDomainId, TitledpersonRow] + def title: Field[TitleId, TitledpersonRow] + def name: Field[String, TitledpersonRow] + def fkTitle: ForeignKey[TitleFields, TitleRow] = + ForeignKey[TitleFields, TitleRow]("public.titledperson_title_fkey", Nil) + .withColumnPair(title, _.code) + def fkTitleDomain: ForeignKey[TitleDomainFields, TitleDomainRow] = + ForeignKey[TitleDomainFields, TitleDomainRow]("public.titledperson_title_short_fkey", Nil) + .withColumnPair(titleShort, _.code) +} + +object TitledpersonFields { + lazy val structure: Relation[TitledpersonFields, TitledpersonRow] = + new Impl(Nil) + + private final class Impl(val _path: List[Path]) + extends Relation[TitledpersonFields, TitledpersonRow] { + + override lazy val fields: TitledpersonFields = new TitledpersonFields { + override def titleShort = Field[TitleDomainId, TitledpersonRow](_path, "title_short", None, Some("text"), x => x.titleShort, (row, value) => row.copy(titleShort = value)) + override def title = Field[TitleId, TitledpersonRow](_path, "title", None, None, x => x.title, (row, value) => row.copy(title = value)) + override def name = Field[String, TitledpersonRow](_path, "name", None, None, x => x.name, (row, value) => row.copy(name = value)) + } + + override lazy val columns: List[FieldLikeNoHkt[?, TitledpersonRow]] = + List[FieldLikeNoHkt[?, TitledpersonRow]](fields.titleShort, fields.title, fields.name) + + override def copy(path: List[Path]): Impl = + new Impl(path) + } + +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala new file mode 100644 index 0000000000..87b51716a4 --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala @@ -0,0 +1,22 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import java.sql.Connection +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder + +trait TitledpersonRepo { + def delete: DeleteBuilder[TitledpersonFields, TitledpersonRow] + def insert(unsaved: TitledpersonRow)(implicit c: Connection): TitledpersonRow + def insertStreaming(unsaved: Iterator[TitledpersonRow], batchSize: Int = 10000)(implicit c: Connection): Long + def select: SelectBuilder[TitledpersonFields, TitledpersonRow] + def selectAll(implicit c: Connection): List[TitledpersonRow] + def update: UpdateBuilder[TitledpersonFields, TitledpersonRow] +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala new file mode 100644 index 0000000000..97d804c6ff --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala @@ -0,0 +1,47 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleId +import adventureworks.public.title_domain.TitleDomainId +import anorm.ParameterValue +import anorm.SqlStringInterpolation +import anorm.ToStatement +import java.sql.Connection +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder + +class TitledpersonRepoImpl extends TitledpersonRepo { + override def delete: DeleteBuilder[TitledpersonFields, TitledpersonRow] = { + DeleteBuilder(""""public"."titledperson"""", TitledpersonFields.structure) + } + override def insert(unsaved: TitledpersonRow)(implicit c: Connection): TitledpersonRow = { + SQL"""insert into "public"."titledperson"("title_short", "title", "name") + values (${ParameterValue(unsaved.titleShort, null, TitleDomainId.toStatement)}::text, ${ParameterValue(unsaved.title, null, TitleId.toStatement)}, ${ParameterValue(unsaved.name, null, ToStatement.stringToStatement)}) + returning "title_short", "title", "name" + """ + .executeInsert(TitledpersonRow.rowParser(1).single) + + } + override def insertStreaming(unsaved: Iterator[TitledpersonRow], batchSize: Int = 10000)(implicit c: Connection): Long = { + streamingInsert(s"""COPY "public"."titledperson"("title_short", "title", "name") FROM STDIN""", batchSize, unsaved)(TitledpersonRow.text, c) + } + override def select: SelectBuilder[TitledpersonFields, TitledpersonRow] = { + SelectBuilderSql(""""public"."titledperson"""", TitledpersonFields.structure, TitledpersonRow.rowParser) + } + override def selectAll(implicit c: Connection): List[TitledpersonRow] = { + SQL"""select "title_short", "title", "name" + from "public"."titledperson" + """.as(TitledpersonRow.rowParser(1).*) + } + override def update: UpdateBuilder[TitledpersonFields, TitledpersonRow] = { + UpdateBuilder(""""public"."titledperson"""", TitledpersonFields.structure, TitledpersonRow.rowParser) + } +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala new file mode 100644 index 0000000000..04e02e4e3e --- /dev/null +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala @@ -0,0 +1,67 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleId +import adventureworks.public.title_domain.TitleDomainId +import anorm.Column +import anorm.RowParser +import anorm.Success +import play.api.libs.json.JsObject +import play.api.libs.json.JsResult +import play.api.libs.json.JsValue +import play.api.libs.json.OWrites +import play.api.libs.json.Reads +import play.api.libs.json.Writes +import scala.collection.immutable.ListMap +import scala.util.Try + +/** Table: public.titledperson */ +case class TitledpersonRow( + /** Points to [[title_domain.TitleDomainRow.code]] */ + titleShort: TitleDomainId, + /** Points to [[title.TitleRow.code]] */ + title: TitleId, + name: String +) + +object TitledpersonRow { + implicit lazy val reads: Reads[TitledpersonRow] = Reads[TitledpersonRow](json => JsResult.fromTry( + Try( + TitledpersonRow( + titleShort = json.\("title_short").as(TitleDomainId.reads), + title = json.\("title").as(TitleId.reads), + name = json.\("name").as(Reads.StringReads) + ) + ) + ), + ) + def rowParser(idx: Int): RowParser[TitledpersonRow] = RowParser[TitledpersonRow] { row => + Success( + TitledpersonRow( + titleShort = row(idx + 0)(TitleDomainId.column), + title = row(idx + 1)(TitleId.column), + name = row(idx + 2)(Column.columnToString) + ) + ) + } + implicit lazy val text: Text[TitledpersonRow] = Text.instance[TitledpersonRow]{ (row, sb) => + TitleDomainId.text.unsafeEncode(row.titleShort, sb) + sb.append(Text.DELIMETER) + TitleId.text.unsafeEncode(row.title, sb) + sb.append(Text.DELIMETER) + Text.stringInstance.unsafeEncode(row.name, sb) + } + implicit lazy val writes: OWrites[TitledpersonRow] = OWrites[TitledpersonRow](o => + new JsObject(ListMap[String, JsValue]( + "title_short" -> TitleDomainId.writes.writes(o.titleShort), + "title" -> TitleId.writes.writes(o.title), + "name" -> Writes.StringWrites.writes(o.name) + )) + ) +} diff --git a/typo-tester-anorm/generated-and-checked-in/adventureworks/testInsert.scala b/typo-tester-anorm/generated-and-checked-in/adventureworks/testInsert.scala index 234398cafb..a648fde625 100644 --- a/typo-tester-anorm/generated-and-checked-in/adventureworks/testInsert.scala +++ b/typo-tester-anorm/generated-and-checked-in/adventureworks/testInsert.scala @@ -208,6 +208,14 @@ import adventureworks.public.pgtest.PgtestRepoImpl import adventureworks.public.pgtest.PgtestRow import adventureworks.public.pgtestnull.PgtestnullRepoImpl import adventureworks.public.pgtestnull.PgtestnullRow +import adventureworks.public.title.TitleId +import adventureworks.public.title.TitleRepoImpl +import adventureworks.public.title.TitleRow +import adventureworks.public.title_domain.TitleDomainId +import adventureworks.public.title_domain.TitleDomainRepoImpl +import adventureworks.public.title_domain.TitleDomainRow +import adventureworks.public.titledperson.TitledpersonRepoImpl +import adventureworks.public.titledperson.TitledpersonRow import adventureworks.public.users.UsersId import adventureworks.public.users.UsersRepoImpl import adventureworks.public.users.UsersRow @@ -773,6 +781,12 @@ class TestInsert(random: Random, domainInsert: TestDomainInsert) { varchares: Option[Array[String]] = if (random.nextBoolean()) None else Some(Array.fill(random.nextInt(3))(random.alphanumeric.take(20).mkString)), xmles: Option[Array[TypoXml]] = None )(implicit c: Connection): PgtestnullRow = (new PgtestnullRepoImpl).insert(new PgtestnullRow(bool = bool, box = box, bpchar = bpchar, bytea = bytea, char = char, circle = circle, date = date, float4 = float4, float8 = float8, hstore = hstore, inet = inet, int2 = int2, int2vector = int2vector, int4 = int4, int8 = int8, interval = interval, json = json, jsonb = jsonb, line = line, lseg = lseg, money = money, mydomain = mydomain, myenum = myenum, name = name, numeric = numeric, path = path, point = point, polygon = polygon, text = text, time = time, timestamp = timestamp, timestampz = timestampz, timez = timez, uuid = uuid, varchar = varchar, vector = vector, xml = xml, boxes = boxes, bpchares = bpchares, chares = chares, circlees = circlees, datees = datees, float4es = float4es, float8es = float8es, inetes = inetes, int2es = int2es, int2vectores = int2vectores, int4es = int4es, int8es = int8es, intervales = intervales, jsones = jsones, jsonbes = jsonbes, linees = linees, lseges = lseges, moneyes = moneyes, mydomaines = mydomaines, myenumes = myenumes, namees = namees, numerices = numerices, pathes = pathes, pointes = pointes, polygones = polygones, textes = textes, timees = timees, timestampes = timestampes, timestampzes = timestampzes, timezes = timezes, uuides = uuides, varchares = varchares, xmles = xmles)) + def publicTitle(code: TitleId = TitleId(random.alphanumeric.take(20).mkString))(implicit c: Connection): TitleRow = (new TitleRepoImpl).insert(new TitleRow(code = code)) + def publicTitleDomain(code: TitleDomainId = TitleDomainId(domainInsert.publicShortText(random)))(implicit c: Connection): TitleDomainRow = (new TitleDomainRepoImpl).insert(new TitleDomainRow(code = code)) + def publicTitledperson(titleShort: TitleDomainId = TitleDomainId.All(random.nextInt(4)), + title: TitleId = TitleId.All(random.nextInt(4)), + name: String = random.alphanumeric.take(20).mkString + )(implicit c: Connection): TitledpersonRow = (new TitledpersonRepoImpl).insert(new TitledpersonRow(titleShort = titleShort, title = title, name = name)) def publicUsers(email: TypoUnknownCitext, userId: UsersId = UsersId(TypoUUID.randomUUID), name: String = random.alphanumeric.take(20).mkString, diff --git a/typo-tester-anorm/src/scala/adventureworks/ArrayTest.scala b/typo-tester-anorm/src/scala/adventureworks/ArrayTest.scala index ffebd18c5b..2fd6c203f9 100644 --- a/typo-tester-anorm/src/scala/adventureworks/ArrayTest.scala +++ b/typo-tester-anorm/src/scala/adventureworks/ArrayTest.scala @@ -4,21 +4,14 @@ import adventureworks.customtypes.* import adventureworks.public.pgtest.{PgtestRepoImpl, PgtestRow} import adventureworks.public.pgtestnull.{PgtestnullRepoImpl, PgtestnullRow} import adventureworks.public.{Mydomain, Myenum} -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.Assertion import org.scalatest.funsuite.AnyFunSuite -import play.api.libs.json.{Json, Writes} import scala.annotation.nowarn -class ArrayTest extends AnyFunSuite with TypeCheckedTripleEquals { +class ArrayTest extends AnyFunSuite with JsonEquals { val pgtestnullRepo: PgtestnullRepoImpl = new PgtestnullRepoImpl val pgtestRepo: PgtestRepoImpl = new PgtestRepoImpl - // need to compare json instead of case classes because of arrays - def assertJsonEquals[A: Writes](a1: A, a2: A): Assertion = - assert(Json.toJson(a1) === Json.toJson(a2)) - test("can insert pgtest rows") { withConnection { implicit c => val before = ArrayTestData.pgTestRow diff --git a/typo-tester-anorm/src/scala/adventureworks/JsonEquals.scala b/typo-tester-anorm/src/scala/adventureworks/JsonEquals.scala new file mode 100644 index 0000000000..14930b6ccf --- /dev/null +++ b/typo-tester-anorm/src/scala/adventureworks/JsonEquals.scala @@ -0,0 +1,13 @@ +package adventureworks + +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.Assertion +import org.scalatest.funsuite.AnyFunSuite +import play.api.libs.json.{Json, Writes} + +trait JsonEquals extends AnyFunSuite with TypeCheckedTripleEquals { + // need to compare json instead of case classes because of arrays + def assertJsonEquals[A: Writes](a1: A, a2: A): Assertion = + assert(Json.toJson(a1) === Json.toJson(a2)) + +} diff --git a/typo-tester-anorm/src/scala/adventureworks/OpenEnumTest.scala b/typo-tester-anorm/src/scala/adventureworks/OpenEnumTest.scala new file mode 100644 index 0000000000..3853058093 --- /dev/null +++ b/typo-tester-anorm/src/scala/adventureworks/OpenEnumTest.scala @@ -0,0 +1,35 @@ +package adventureworks + +import adventureworks.public.title.* +import adventureworks.public.title_domain.* +import adventureworks.public.titledperson.* +import org.scalatest.funsuite.AnyFunSuite +import typo.dsl.ToTupleOps + +import scala.util.Random + +class OpenEnumTest extends AnyFunSuite with JsonEquals { + val titleRepo = new TitleRepoImpl + val titleDomainRepo = new TitleDomainRepoImpl + val titledPersonRepo = new TitledpersonRepoImpl + + test("works") { + withConnection { implicit c => + val testInsert = new TestInsert(new Random(0), DomainInsert) + val john = testInsert.publicTitledperson(TitleDomainId.dr, TitleId.dr, "John") + + val found: Option[((TitledpersonRow, TitleRow), TitleDomainRow)] = + titledPersonRepo.select + .joinFk(_.fkTitle)(titleRepo.select.where(_.code.in(Array(TitleId.dr)))) + .joinFk(_._1.fkTitleDomain)(titleDomainRepo.select.where(_.code.in(Array(TitleDomainId.dr)))) + .where { case ((tp, _), _) => tp.name === "John" } + .toList + .headOption + + val expected: Option[((TitledpersonRow, TitleRow), TitleDomainRow)] = + Option(john ~ TitleRow(TitleId.dr) ~ TitleDomainRow(TitleDomainId.dr)) + + assertJsonEquals(found, expected) + } + } +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleFields.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleFields.scala new file mode 100644 index 0000000000..a8ed6e549d --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleFields.scala @@ -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) + } + +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleId.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleId.scala new file mode 100644 index 0000000000..6a2ac39805 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleId.scala @@ -0,0 +1,51 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import doobie.postgres.Text +import doobie.util.Get +import doobie.util.Put +import doobie.util.Read +import doobie.util.Write +import doobie.util.meta.Meta +import io.circe.Decoder +import io.circe.Encoder + +/** 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 arrayGet: Get[Array[TitleId]] = adventureworks.StringArrayMeta.get.map(_.map(TitleId.apply)) + implicit lazy val arrayPut: Put[Array[TitleId]] = adventureworks.StringArrayMeta.put.contramap(_.map(_.value)) + implicit lazy val decoder: Decoder[TitleId] = Decoder.decodeString.map(TitleId.apply) + implicit lazy val encoder: Encoder[TitleId] = Encoder.encodeString.contramap(_.value) + implicit lazy val get: Get[TitleId] = Meta.StringMeta.get.map(TitleId.apply) + implicit lazy val ordering: Ordering[TitleId] = Ordering.by(_.value) + implicit lazy val put: Put[TitleId] = Meta.StringMeta.put.contramap(_.value) + implicit lazy val read: Read[TitleId] = Read.fromGet(get) + 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 write: Write[TitleId] = Write.fromPut(put) +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala new file mode 100644 index 0000000000..db8b452677 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala @@ -0,0 +1,32 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import doobie.free.connection.ConnectionIO +import fs2.Stream +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder + +trait TitleRepo { + def delete: DeleteBuilder[TitleFields, TitleRow] + def deleteById(code: TitleId): ConnectionIO[Boolean] + def deleteByIds(codes: Array[TitleId]): ConnectionIO[Int] + def insert(unsaved: TitleRow): ConnectionIO[TitleRow] + def insertStreaming(unsaved: Stream[ConnectionIO, TitleRow], batchSize: Int = 10000): ConnectionIO[Long] + def select: SelectBuilder[TitleFields, TitleRow] + def selectAll: Stream[ConnectionIO, TitleRow] + def selectById(code: TitleId): ConnectionIO[Option[TitleRow]] + def selectByIds(codes: Array[TitleId]): Stream[ConnectionIO, TitleRow] + def selectByIdsTracked(codes: Array[TitleId]): ConnectionIO[Map[TitleId, TitleRow]] + def update: UpdateBuilder[TitleFields, TitleRow] + def upsert(unsaved: TitleRow): ConnectionIO[TitleRow] + def upsertBatch(unsaved: List[TitleRow]): Stream[ConnectionIO, TitleRow] + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + def upsertStreaming(unsaved: Stream[ConnectionIO, TitleRow], batchSize: Int = 10000): ConnectionIO[Int] +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala new file mode 100644 index 0000000000..4652de6364 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala @@ -0,0 +1,96 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import cats.instances.list.catsStdInstancesForList +import doobie.free.connection.ConnectionIO +import doobie.postgres.syntax.FragmentOps +import doobie.syntax.SqlInterpolator.SingleFragment.fromWrite +import doobie.syntax.string.toSqlInterpolator +import doobie.util.Write +import doobie.util.update.Update +import fs2.Stream +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder + +class TitleRepoImpl extends TitleRepo { + override def delete: DeleteBuilder[TitleFields, TitleRow] = { + DeleteBuilder(""""public"."title"""", TitleFields.structure) + } + override def deleteById(code: TitleId): ConnectionIO[Boolean] = { + sql"""delete from "public"."title" where "code" = ${fromWrite(code)(Write.fromPut(TitleId.put))}""".update.run.map(_ > 0) + } + override def deleteByIds(codes: Array[TitleId]): ConnectionIO[Int] = { + sql"""delete from "public"."title" where "code" = ANY(${codes})""".update.run + } + override def insert(unsaved: TitleRow): ConnectionIO[TitleRow] = { + sql"""insert into "public"."title"("code") + values (${fromWrite(unsaved.code)(Write.fromPut(TitleId.put))}) + returning "code" + """.query(using TitleRow.read).unique + } + override def insertStreaming(unsaved: Stream[ConnectionIO, TitleRow], batchSize: Int = 10000): ConnectionIO[Long] = { + new FragmentOps(sql"""COPY "public"."title"("code") FROM STDIN""").copyIn(unsaved, batchSize)(using TitleRow.text) + } + override def select: SelectBuilder[TitleFields, TitleRow] = { + SelectBuilderSql(""""public"."title"""", TitleFields.structure, TitleRow.read) + } + override def selectAll: Stream[ConnectionIO, TitleRow] = { + sql"""select "code" from "public"."title"""".query(using TitleRow.read).stream + } + override def selectById(code: TitleId): ConnectionIO[Option[TitleRow]] = { + sql"""select "code" from "public"."title" where "code" = ${fromWrite(code)(Write.fromPut(TitleId.put))}""".query(using TitleRow.read).option + } + override def selectByIds(codes: Array[TitleId]): Stream[ConnectionIO, TitleRow] = { + sql"""select "code" from "public"."title" where "code" = ANY(${codes})""".query(using TitleRow.read).stream + } + override def selectByIdsTracked(codes: Array[TitleId]): ConnectionIO[Map[TitleId, TitleRow]] = { + selectByIds(codes).compile.toList.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleFields, TitleRow] = { + UpdateBuilder(""""public"."title"""", TitleFields.structure, TitleRow.read) + } + override def upsert(unsaved: TitleRow): ConnectionIO[TitleRow] = { + sql"""insert into "public"."title"("code") + values ( + ${fromWrite(unsaved.code)(Write.fromPut(TitleId.put))} + ) + on conflict ("code") + do nothing + returning "code" + """.query(using TitleRow.read).unique + } + override def upsertBatch(unsaved: List[TitleRow]): Stream[ConnectionIO, TitleRow] = { + Update[TitleRow]( + s"""insert into "public"."title"("code") + values (?) + on conflict ("code") + do nothing + returning "code"""" + )(using TitleRow.write) + .updateManyWithGeneratedKeys[TitleRow]("code")(unsaved)(using catsStdInstancesForList, TitleRow.read) + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Stream[ConnectionIO, TitleRow], batchSize: Int = 10000): ConnectionIO[Int] = { + for { + _ <- sql"""create temporary table title_TEMP (like "public"."title") on commit drop""".update.run + _ <- new FragmentOps(sql"""copy title_TEMP("code") from stdin""").copyIn(unsaved, batchSize)(using TitleRow.text) + res <- sql"""insert into "public"."title"("code") + select * from title_TEMP + on conflict ("code") + do nothing + ; + drop table title_TEMP;""".update.run + } yield res + } +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala new file mode 100644 index 0000000000..6df904f23b --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala @@ -0,0 +1,100 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import doobie.free.connection.ConnectionIO +import doobie.free.connection.delay +import fs2.Stream +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.DeleteBuilder.DeleteBuilderMock +import typo.dsl.DeleteParams +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderMock +import typo.dsl.SelectParams +import typo.dsl.UpdateBuilder +import typo.dsl.UpdateBuilder.UpdateBuilderMock +import typo.dsl.UpdateParams + +class TitleRepoMock(map: scala.collection.mutable.Map[TitleId, TitleRow] = scala.collection.mutable.Map.empty) extends TitleRepo { + override def delete: DeleteBuilder[TitleFields, TitleRow] = { + DeleteBuilderMock(DeleteParams.empty, TitleFields.structure, map) + } + override def deleteById(code: TitleId): ConnectionIO[Boolean] = { + delay(map.remove(code).isDefined) + } + override def deleteByIds(codes: Array[TitleId]): ConnectionIO[Int] = { + delay(codes.map(id => map.remove(id)).count(_.isDefined)) + } + override def insert(unsaved: TitleRow): ConnectionIO[TitleRow] = { + delay { + val _ = if (map.contains(unsaved.code)) + sys.error(s"id ${unsaved.code} already exists") + else + map.put(unsaved.code, unsaved) + + unsaved + } + } + override def insertStreaming(unsaved: Stream[ConnectionIO, TitleRow], batchSize: Int = 10000): ConnectionIO[Long] = { + unsaved.compile.toList.map { rows => + var num = 0L + rows.foreach { row => + map += (row.code -> row) + num += 1 + } + num + } + } + override def select: SelectBuilder[TitleFields, TitleRow] = { + SelectBuilderMock(TitleFields.structure, delay(map.values.toList), SelectParams.empty) + } + override def selectAll: Stream[ConnectionIO, TitleRow] = { + Stream.emits(map.values.toList) + } + override def selectById(code: TitleId): ConnectionIO[Option[TitleRow]] = { + delay(map.get(code)) + } + override def selectByIds(codes: Array[TitleId]): Stream[ConnectionIO, TitleRow] = { + Stream.emits(codes.flatMap(map.get).toList) + } + override def selectByIdsTracked(codes: Array[TitleId]): ConnectionIO[Map[TitleId, TitleRow]] = { + selectByIds(codes).compile.toList.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleFields, TitleRow] = { + UpdateBuilderMock(UpdateParams.empty, TitleFields.structure, map) + } + override def upsert(unsaved: TitleRow): ConnectionIO[TitleRow] = { + delay { + map.put(unsaved.code, unsaved): @nowarn + unsaved + } + } + override def upsertBatch(unsaved: List[TitleRow]): Stream[ConnectionIO, TitleRow] = { + Stream.emits { + unsaved.map { row => + map += (row.code -> row) + row + } + } + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Stream[ConnectionIO, TitleRow], batchSize: Int = 10000): ConnectionIO[Int] = { + unsaved.compile.toList.map { rows => + var num = 0 + rows.foreach { row => + map += (row.code -> row) + num += 1 + } + num + } + } +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRow.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRow.scala new file mode 100644 index 0000000000..f669817066 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title/TitleRow.scala @@ -0,0 +1,50 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import doobie.enumerated.Nullability +import doobie.postgres.Text +import doobie.util.Read +import doobie.util.Write +import io.circe.Decoder +import io.circe.Encoder +import java.sql.ResultSet + +/** Table: public.title + Primary key: code */ +case class TitleRow( + code: TitleId +){ + val id = code + } + +object TitleRow { + implicit lazy val decoder: Decoder[TitleRow] = Decoder.forProduct1[TitleRow, TitleId]("code")(TitleRow.apply)(TitleId.decoder) + implicit lazy val encoder: Encoder[TitleRow] = Encoder.forProduct1[TitleRow, TitleId]("code")(x => (x.code))(TitleId.encoder) + implicit lazy val read: Read[TitleRow] = new Read[TitleRow]( + gets = List( + (TitleId.get, Nullability.NoNulls) + ), + unsafeGet = (rs: ResultSet, i: Int) => TitleRow( + code = TitleId.get.unsafeGetNonNullable(rs, i + 0) + ) + ) + implicit lazy val text: Text[TitleRow] = Text.instance[TitleRow]{ (row, sb) => + TitleId.text.unsafeEncode(row.code, sb) + } + implicit lazy val write: Write[TitleRow] = new Write[TitleRow]( + puts = List((TitleId.put, Nullability.NoNulls)), + toList = x => List(x.code), + unsafeSet = (rs, i, a) => { + TitleId.put.unsafeSetNonNullable(rs, i + 0, a.code) + }, + unsafeUpdate = (ps, i, a) => { + TitleId.put.unsafeUpdateNonNullable(ps, i + 0, a.code) + } + ) +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala new file mode 100644 index 0000000000..c87f1bab4b --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala @@ -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_domain + +import typo.dsl.Path +import typo.dsl.SqlExpr.FieldLikeNoHkt +import typo.dsl.SqlExpr.IdField +import typo.dsl.Structure.Relation + +trait TitleDomainFields { + def code: IdField[TitleDomainId, TitleDomainRow] +} + +object TitleDomainFields { + lazy val structure: Relation[TitleDomainFields, TitleDomainRow] = + new Impl(Nil) + + private final class Impl(val _path: List[Path]) + extends Relation[TitleDomainFields, TitleDomainRow] { + + override lazy val fields: TitleDomainFields = new TitleDomainFields { + override def code = IdField[TitleDomainId, TitleDomainRow](_path, "code", None, Some("text"), x => x.code, (row, value) => row.copy(code = value)) + } + + override lazy val columns: List[FieldLikeNoHkt[?, TitleDomainRow]] = + List[FieldLikeNoHkt[?, TitleDomainRow]](fields.code) + + override def copy(path: List[Path]): Impl = + new Impl(path) + } + +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala new file mode 100644 index 0000000000..4232f77fc4 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala @@ -0,0 +1,51 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import doobie.postgres.Text +import doobie.util.Get +import doobie.util.Put +import doobie.util.Read +import doobie.util.Write +import io.circe.Decoder +import io.circe.Encoder + +/** Type for the primary key of table `public.title_domain`. It has some known values: + * - dr + * - mr + * - ms + * - phd + */ +sealed abstract class TitleDomainId(val value: ShortText) + +object TitleDomainId { + def apply(underlying: ShortText): TitleDomainId = + ByName.getOrElse(underlying, Unknown(underlying)) + def shortText(value: String): TitleDomainId = TitleDomainId(ShortText(value)) + case object dr extends TitleDomainId(ShortText("dr")) + case object mr extends TitleDomainId(ShortText("mr")) + case object ms extends TitleDomainId(ShortText("ms")) + case object phd extends TitleDomainId(ShortText("phd")) + case class Unknown(override val value: ShortText) extends TitleDomainId(value) + val All: List[TitleDomainId] = List(dr, mr, ms, phd) + val ByName: Map[ShortText, TitleDomainId] = All.map(x => (x.value, x)).toMap + + implicit lazy val arrayGet: Get[Array[TitleDomainId]] = ShortText.arrayGet.map(_.map(TitleDomainId.apply)) + implicit lazy val arrayPut: Put[Array[TitleDomainId]] = ShortText.arrayPut.contramap(_.map(_.value)) + implicit lazy val decoder: Decoder[TitleDomainId] = ShortText.decoder.map(TitleDomainId.apply) + implicit lazy val encoder: Encoder[TitleDomainId] = ShortText.encoder.contramap(_.value) + implicit lazy val get: Get[TitleDomainId] = ShortText.get.map(TitleDomainId.apply) + implicit lazy val ordering: Ordering[TitleDomainId] = Ordering.by(_.value) + implicit lazy val put: Put[TitleDomainId] = ShortText.put.contramap(_.value) + implicit lazy val read: Read[TitleDomainId] = Read.fromGet(get) + implicit lazy val text: Text[TitleDomainId] = new Text[TitleDomainId] { + override def unsafeEncode(v: TitleDomainId, sb: StringBuilder) = ShortText.text.unsafeEncode(v.value, sb) + override def unsafeArrayEncode(v: TitleDomainId, sb: StringBuilder) = ShortText.text.unsafeArrayEncode(v.value, sb) + } + implicit lazy val write: Write[TitleDomainId] = Write.fromPut(put) +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala new file mode 100644 index 0000000000..8dd1139dae --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala @@ -0,0 +1,32 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import doobie.free.connection.ConnectionIO +import fs2.Stream +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder + +trait TitleDomainRepo { + def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] + def deleteById(code: TitleDomainId): ConnectionIO[Boolean] + def deleteByIds(codes: Array[TitleDomainId]): ConnectionIO[Int] + def insert(unsaved: TitleDomainRow): ConnectionIO[TitleDomainRow] + def insertStreaming(unsaved: Stream[ConnectionIO, TitleDomainRow], batchSize: Int = 10000): ConnectionIO[Long] + def select: SelectBuilder[TitleDomainFields, TitleDomainRow] + def selectAll: Stream[ConnectionIO, TitleDomainRow] + def selectById(code: TitleDomainId): ConnectionIO[Option[TitleDomainRow]] + def selectByIds(codes: Array[TitleDomainId]): Stream[ConnectionIO, TitleDomainRow] + def selectByIdsTracked(codes: Array[TitleDomainId]): ConnectionIO[Map[TitleDomainId, TitleDomainRow]] + def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] + def upsert(unsaved: TitleDomainRow): ConnectionIO[TitleDomainRow] + def upsertBatch(unsaved: List[TitleDomainRow]): Stream[ConnectionIO, TitleDomainRow] + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + def upsertStreaming(unsaved: Stream[ConnectionIO, TitleDomainRow], batchSize: Int = 10000): ConnectionIO[Int] +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala new file mode 100644 index 0000000000..5879cd3581 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala @@ -0,0 +1,96 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import cats.instances.list.catsStdInstancesForList +import doobie.free.connection.ConnectionIO +import doobie.postgres.syntax.FragmentOps +import doobie.syntax.SqlInterpolator.SingleFragment.fromWrite +import doobie.syntax.string.toSqlInterpolator +import doobie.util.Write +import doobie.util.update.Update +import fs2.Stream +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder + +class TitleDomainRepoImpl extends TitleDomainRepo { + override def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] = { + DeleteBuilder(""""public"."title_domain"""", TitleDomainFields.structure) + } + override def deleteById(code: TitleDomainId): ConnectionIO[Boolean] = { + sql"""delete from "public"."title_domain" where "code" = ${fromWrite(code)(Write.fromPut(TitleDomainId.put))}""".update.run.map(_ > 0) + } + override def deleteByIds(codes: Array[TitleDomainId]): ConnectionIO[Int] = { + sql"""delete from "public"."title_domain" where "code" = ANY(${codes})""".update.run + } + override def insert(unsaved: TitleDomainRow): ConnectionIO[TitleDomainRow] = { + sql"""insert into "public"."title_domain"("code") + values (${fromWrite(unsaved.code)(Write.fromPut(TitleDomainId.put))}::text) + returning "code" + """.query(using TitleDomainRow.read).unique + } + override def insertStreaming(unsaved: Stream[ConnectionIO, TitleDomainRow], batchSize: Int = 10000): ConnectionIO[Long] = { + new FragmentOps(sql"""COPY "public"."title_domain"("code") FROM STDIN""").copyIn(unsaved, batchSize)(using TitleDomainRow.text) + } + override def select: SelectBuilder[TitleDomainFields, TitleDomainRow] = { + SelectBuilderSql(""""public"."title_domain"""", TitleDomainFields.structure, TitleDomainRow.read) + } + override def selectAll: Stream[ConnectionIO, TitleDomainRow] = { + sql"""select "code" from "public"."title_domain"""".query(using TitleDomainRow.read).stream + } + override def selectById(code: TitleDomainId): ConnectionIO[Option[TitleDomainRow]] = { + sql"""select "code" from "public"."title_domain" where "code" = ${fromWrite(code)(Write.fromPut(TitleDomainId.put))}""".query(using TitleDomainRow.read).option + } + override def selectByIds(codes: Array[TitleDomainId]): Stream[ConnectionIO, TitleDomainRow] = { + sql"""select "code" from "public"."title_domain" where "code" = ANY(${codes})""".query(using TitleDomainRow.read).stream + } + override def selectByIdsTracked(codes: Array[TitleDomainId]): ConnectionIO[Map[TitleDomainId, TitleDomainRow]] = { + selectByIds(codes).compile.toList.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] = { + UpdateBuilder(""""public"."title_domain"""", TitleDomainFields.structure, TitleDomainRow.read) + } + override def upsert(unsaved: TitleDomainRow): ConnectionIO[TitleDomainRow] = { + sql"""insert into "public"."title_domain"("code") + values ( + ${fromWrite(unsaved.code)(Write.fromPut(TitleDomainId.put))}::text + ) + on conflict ("code") + do nothing + returning "code" + """.query(using TitleDomainRow.read).unique + } + override def upsertBatch(unsaved: List[TitleDomainRow]): Stream[ConnectionIO, TitleDomainRow] = { + Update[TitleDomainRow]( + s"""insert into "public"."title_domain"("code") + values (?::text) + on conflict ("code") + do nothing + returning "code"""" + )(using TitleDomainRow.write) + .updateManyWithGeneratedKeys[TitleDomainRow]("code")(unsaved)(using catsStdInstancesForList, TitleDomainRow.read) + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Stream[ConnectionIO, TitleDomainRow], batchSize: Int = 10000): ConnectionIO[Int] = { + for { + _ <- sql"""create temporary table title_domain_TEMP (like "public"."title_domain") on commit drop""".update.run + _ <- new FragmentOps(sql"""copy title_domain_TEMP("code") from stdin""").copyIn(unsaved, batchSize)(using TitleDomainRow.text) + res <- sql"""insert into "public"."title_domain"("code") + select * from title_domain_TEMP + on conflict ("code") + do nothing + ; + drop table title_domain_TEMP;""".update.run + } yield res + } +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala new file mode 100644 index 0000000000..27c6004b5f --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala @@ -0,0 +1,100 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import doobie.free.connection.ConnectionIO +import doobie.free.connection.delay +import fs2.Stream +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.DeleteBuilder.DeleteBuilderMock +import typo.dsl.DeleteParams +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderMock +import typo.dsl.SelectParams +import typo.dsl.UpdateBuilder +import typo.dsl.UpdateBuilder.UpdateBuilderMock +import typo.dsl.UpdateParams + +class TitleDomainRepoMock(map: scala.collection.mutable.Map[TitleDomainId, TitleDomainRow] = scala.collection.mutable.Map.empty) extends TitleDomainRepo { + override def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] = { + DeleteBuilderMock(DeleteParams.empty, TitleDomainFields.structure, map) + } + override def deleteById(code: TitleDomainId): ConnectionIO[Boolean] = { + delay(map.remove(code).isDefined) + } + override def deleteByIds(codes: Array[TitleDomainId]): ConnectionIO[Int] = { + delay(codes.map(id => map.remove(id)).count(_.isDefined)) + } + override def insert(unsaved: TitleDomainRow): ConnectionIO[TitleDomainRow] = { + delay { + val _ = if (map.contains(unsaved.code)) + sys.error(s"id ${unsaved.code} already exists") + else + map.put(unsaved.code, unsaved) + + unsaved + } + } + override def insertStreaming(unsaved: Stream[ConnectionIO, TitleDomainRow], batchSize: Int = 10000): ConnectionIO[Long] = { + unsaved.compile.toList.map { rows => + var num = 0L + rows.foreach { row => + map += (row.code -> row) + num += 1 + } + num + } + } + override def select: SelectBuilder[TitleDomainFields, TitleDomainRow] = { + SelectBuilderMock(TitleDomainFields.structure, delay(map.values.toList), SelectParams.empty) + } + override def selectAll: Stream[ConnectionIO, TitleDomainRow] = { + Stream.emits(map.values.toList) + } + override def selectById(code: TitleDomainId): ConnectionIO[Option[TitleDomainRow]] = { + delay(map.get(code)) + } + override def selectByIds(codes: Array[TitleDomainId]): Stream[ConnectionIO, TitleDomainRow] = { + Stream.emits(codes.flatMap(map.get).toList) + } + override def selectByIdsTracked(codes: Array[TitleDomainId]): ConnectionIO[Map[TitleDomainId, TitleDomainRow]] = { + selectByIds(codes).compile.toList.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] = { + UpdateBuilderMock(UpdateParams.empty, TitleDomainFields.structure, map) + } + override def upsert(unsaved: TitleDomainRow): ConnectionIO[TitleDomainRow] = { + delay { + map.put(unsaved.code, unsaved): @nowarn + unsaved + } + } + override def upsertBatch(unsaved: List[TitleDomainRow]): Stream[ConnectionIO, TitleDomainRow] = { + Stream.emits { + unsaved.map { row => + map += (row.code -> row) + row + } + } + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: Stream[ConnectionIO, TitleDomainRow], batchSize: Int = 10000): ConnectionIO[Int] = { + unsaved.compile.toList.map { rows => + var num = 0 + rows.foreach { row => + map += (row.code -> row) + num += 1 + } + num + } + } +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala new file mode 100644 index 0000000000..a902169cd8 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala @@ -0,0 +1,50 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import doobie.enumerated.Nullability +import doobie.postgres.Text +import doobie.util.Read +import doobie.util.Write +import io.circe.Decoder +import io.circe.Encoder +import java.sql.ResultSet + +/** Table: public.title_domain + Primary key: code */ +case class TitleDomainRow( + code: TitleDomainId +){ + val id = code + } + +object TitleDomainRow { + implicit lazy val decoder: Decoder[TitleDomainRow] = Decoder.forProduct1[TitleDomainRow, TitleDomainId]("code")(TitleDomainRow.apply)(TitleDomainId.decoder) + implicit lazy val encoder: Encoder[TitleDomainRow] = Encoder.forProduct1[TitleDomainRow, TitleDomainId]("code")(x => (x.code))(TitleDomainId.encoder) + implicit lazy val read: Read[TitleDomainRow] = new Read[TitleDomainRow]( + gets = List( + (TitleDomainId.get, Nullability.NoNulls) + ), + unsafeGet = (rs: ResultSet, i: Int) => TitleDomainRow( + code = TitleDomainId.get.unsafeGetNonNullable(rs, i + 0) + ) + ) + implicit lazy val text: Text[TitleDomainRow] = Text.instance[TitleDomainRow]{ (row, sb) => + TitleDomainId.text.unsafeEncode(row.code, sb) + } + implicit lazy val write: Write[TitleDomainRow] = new Write[TitleDomainRow]( + puts = List((TitleDomainId.put, Nullability.NoNulls)), + toList = x => List(x.code), + unsafeSet = (rs, i, a) => { + TitleDomainId.put.unsafeSetNonNullable(rs, i + 0, a.code) + }, + unsafeUpdate = (ps, i, a) => { + TitleDomainId.put.unsafeUpdateNonNullable(ps, i + 0, a.code) + } + ) +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala new file mode 100644 index 0000000000..5b42488ef4 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala @@ -0,0 +1,54 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleFields +import adventureworks.public.title.TitleId +import adventureworks.public.title.TitleRow +import adventureworks.public.title_domain.TitleDomainFields +import adventureworks.public.title_domain.TitleDomainId +import adventureworks.public.title_domain.TitleDomainRow +import typo.dsl.ForeignKey +import typo.dsl.Path +import typo.dsl.SqlExpr.Field +import typo.dsl.SqlExpr.FieldLikeNoHkt +import typo.dsl.Structure.Relation + +trait TitledpersonFields { + def titleShort: Field[TitleDomainId, TitledpersonRow] + def title: Field[TitleId, TitledpersonRow] + def name: Field[String, TitledpersonRow] + def fkTitle: ForeignKey[TitleFields, TitleRow] = + ForeignKey[TitleFields, TitleRow]("public.titledperson_title_fkey", Nil) + .withColumnPair(title, _.code) + def fkTitleDomain: ForeignKey[TitleDomainFields, TitleDomainRow] = + ForeignKey[TitleDomainFields, TitleDomainRow]("public.titledperson_title_short_fkey", Nil) + .withColumnPair(titleShort, _.code) +} + +object TitledpersonFields { + lazy val structure: Relation[TitledpersonFields, TitledpersonRow] = + new Impl(Nil) + + private final class Impl(val _path: List[Path]) + extends Relation[TitledpersonFields, TitledpersonRow] { + + override lazy val fields: TitledpersonFields = new TitledpersonFields { + override def titleShort = Field[TitleDomainId, TitledpersonRow](_path, "title_short", None, Some("text"), x => x.titleShort, (row, value) => row.copy(titleShort = value)) + override def title = Field[TitleId, TitledpersonRow](_path, "title", None, None, x => x.title, (row, value) => row.copy(title = value)) + override def name = Field[String, TitledpersonRow](_path, "name", None, None, x => x.name, (row, value) => row.copy(name = value)) + } + + override lazy val columns: List[FieldLikeNoHkt[?, TitledpersonRow]] = + List[FieldLikeNoHkt[?, TitledpersonRow]](fields.titleShort, fields.title, fields.name) + + override def copy(path: List[Path]): Impl = + new Impl(path) + } + +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala new file mode 100644 index 0000000000..5f3002fc35 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala @@ -0,0 +1,23 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import doobie.free.connection.ConnectionIO +import fs2.Stream +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder + +trait TitledpersonRepo { + def delete: DeleteBuilder[TitledpersonFields, TitledpersonRow] + def insert(unsaved: TitledpersonRow): ConnectionIO[TitledpersonRow] + def insertStreaming(unsaved: Stream[ConnectionIO, TitledpersonRow], batchSize: Int = 10000): ConnectionIO[Long] + def select: SelectBuilder[TitledpersonFields, TitledpersonRow] + def selectAll: Stream[ConnectionIO, TitledpersonRow] + def update: UpdateBuilder[TitledpersonFields, TitledpersonRow] +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala new file mode 100644 index 0000000000..22aad81606 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala @@ -0,0 +1,46 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleId +import adventureworks.public.title_domain.TitleDomainId +import doobie.free.connection.ConnectionIO +import doobie.postgres.syntax.FragmentOps +import doobie.syntax.SqlInterpolator.SingleFragment.fromWrite +import doobie.syntax.string.toSqlInterpolator +import doobie.util.Write +import doobie.util.meta.Meta +import fs2.Stream +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder + +class TitledpersonRepoImpl extends TitledpersonRepo { + override def delete: DeleteBuilder[TitledpersonFields, TitledpersonRow] = { + DeleteBuilder(""""public"."titledperson"""", TitledpersonFields.structure) + } + override def insert(unsaved: TitledpersonRow): ConnectionIO[TitledpersonRow] = { + sql"""insert into "public"."titledperson"("title_short", "title", "name") + values (${fromWrite(unsaved.titleShort)(Write.fromPut(TitleDomainId.put))}::text, ${fromWrite(unsaved.title)(Write.fromPut(TitleId.put))}, ${fromWrite(unsaved.name)(Write.fromPut(Meta.StringMeta.put))}) + returning "title_short", "title", "name" + """.query(using TitledpersonRow.read).unique + } + override def insertStreaming(unsaved: Stream[ConnectionIO, TitledpersonRow], batchSize: Int = 10000): ConnectionIO[Long] = { + new FragmentOps(sql"""COPY "public"."titledperson"("title_short", "title", "name") FROM STDIN""").copyIn(unsaved, batchSize)(using TitledpersonRow.text) + } + override def select: SelectBuilder[TitledpersonFields, TitledpersonRow] = { + SelectBuilderSql(""""public"."titledperson"""", TitledpersonFields.structure, TitledpersonRow.read) + } + override def selectAll: Stream[ConnectionIO, TitledpersonRow] = { + sql"""select "title_short", "title", "name" from "public"."titledperson"""".query(using TitledpersonRow.read).stream + } + override def update: UpdateBuilder[TitledpersonFields, TitledpersonRow] = { + UpdateBuilder(""""public"."titledperson"""", TitledpersonFields.structure, TitledpersonRow.read) + } +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala new file mode 100644 index 0000000000..5ebf689ff4 --- /dev/null +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala @@ -0,0 +1,68 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleId +import adventureworks.public.title_domain.TitleDomainId +import doobie.enumerated.Nullability +import doobie.postgres.Text +import doobie.util.Read +import doobie.util.Write +import doobie.util.meta.Meta +import io.circe.Decoder +import io.circe.Encoder +import java.sql.ResultSet + +/** Table: public.titledperson */ +case class TitledpersonRow( + /** Points to [[title_domain.TitleDomainRow.code]] */ + titleShort: TitleDomainId, + /** Points to [[title.TitleRow.code]] */ + title: TitleId, + name: String +) + +object TitledpersonRow { + implicit lazy val decoder: Decoder[TitledpersonRow] = Decoder.forProduct3[TitledpersonRow, TitleDomainId, TitleId, String]("title_short", "title", "name")(TitledpersonRow.apply)(TitleDomainId.decoder, TitleId.decoder, Decoder.decodeString) + implicit lazy val encoder: Encoder[TitledpersonRow] = Encoder.forProduct3[TitledpersonRow, TitleDomainId, TitleId, String]("title_short", "title", "name")(x => (x.titleShort, x.title, x.name))(TitleDomainId.encoder, TitleId.encoder, Encoder.encodeString) + implicit lazy val read: Read[TitledpersonRow] = new Read[TitledpersonRow]( + gets = List( + (TitleDomainId.get, Nullability.NoNulls), + (TitleId.get, Nullability.NoNulls), + (Meta.StringMeta.get, Nullability.NoNulls) + ), + unsafeGet = (rs: ResultSet, i: Int) => TitledpersonRow( + titleShort = TitleDomainId.get.unsafeGetNonNullable(rs, i + 0), + title = TitleId.get.unsafeGetNonNullable(rs, i + 1), + name = Meta.StringMeta.get.unsafeGetNonNullable(rs, i + 2) + ) + ) + implicit lazy val text: Text[TitledpersonRow] = Text.instance[TitledpersonRow]{ (row, sb) => + TitleDomainId.text.unsafeEncode(row.titleShort, sb) + sb.append(Text.DELIMETER) + TitleId.text.unsafeEncode(row.title, sb) + sb.append(Text.DELIMETER) + Text.stringInstance.unsafeEncode(row.name, sb) + } + implicit lazy val write: Write[TitledpersonRow] = new Write[TitledpersonRow]( + puts = List((TitleDomainId.put, Nullability.NoNulls), + (TitleId.put, Nullability.NoNulls), + (Meta.StringMeta.put, Nullability.NoNulls)), + toList = x => List(x.titleShort, x.title, x.name), + unsafeSet = (rs, i, a) => { + TitleDomainId.put.unsafeSetNonNullable(rs, i + 0, a.titleShort) + TitleId.put.unsafeSetNonNullable(rs, i + 1, a.title) + Meta.StringMeta.put.unsafeSetNonNullable(rs, i + 2, a.name) + }, + unsafeUpdate = (ps, i, a) => { + TitleDomainId.put.unsafeUpdateNonNullable(ps, i + 0, a.titleShort) + TitleId.put.unsafeUpdateNonNullable(ps, i + 1, a.title) + Meta.StringMeta.put.unsafeUpdateNonNullable(ps, i + 2, a.name) + } + ) +} diff --git a/typo-tester-doobie/generated-and-checked-in/adventureworks/testInsert.scala b/typo-tester-doobie/generated-and-checked-in/adventureworks/testInsert.scala index 814d07ee3d..43a30e32d1 100644 --- a/typo-tester-doobie/generated-and-checked-in/adventureworks/testInsert.scala +++ b/typo-tester-doobie/generated-and-checked-in/adventureworks/testInsert.scala @@ -208,6 +208,14 @@ import adventureworks.public.pgtest.PgtestRepoImpl import adventureworks.public.pgtest.PgtestRow import adventureworks.public.pgtestnull.PgtestnullRepoImpl import adventureworks.public.pgtestnull.PgtestnullRow +import adventureworks.public.title.TitleId +import adventureworks.public.title.TitleRepoImpl +import adventureworks.public.title.TitleRow +import adventureworks.public.title_domain.TitleDomainId +import adventureworks.public.title_domain.TitleDomainRepoImpl +import adventureworks.public.title_domain.TitleDomainRow +import adventureworks.public.titledperson.TitledpersonRepoImpl +import adventureworks.public.titledperson.TitledpersonRow import adventureworks.public.users.UsersId import adventureworks.public.users.UsersRepoImpl import adventureworks.public.users.UsersRow @@ -773,6 +781,12 @@ class TestInsert(random: Random, domainInsert: TestDomainInsert) { varchares: Option[Array[String]] = if (random.nextBoolean()) None else Some(Array.fill(random.nextInt(3))(random.alphanumeric.take(20).mkString)), xmles: Option[Array[TypoXml]] = None ): ConnectionIO[PgtestnullRow] = (new PgtestnullRepoImpl).insert(new PgtestnullRow(bool = bool, box = box, bpchar = bpchar, bytea = bytea, char = char, circle = circle, date = date, float4 = float4, float8 = float8, hstore = hstore, inet = inet, int2 = int2, int2vector = int2vector, int4 = int4, int8 = int8, interval = interval, json = json, jsonb = jsonb, line = line, lseg = lseg, money = money, mydomain = mydomain, myenum = myenum, name = name, numeric = numeric, path = path, point = point, polygon = polygon, text = text, time = time, timestamp = timestamp, timestampz = timestampz, timez = timez, uuid = uuid, varchar = varchar, vector = vector, xml = xml, boxes = boxes, bpchares = bpchares, chares = chares, circlees = circlees, datees = datees, float4es = float4es, float8es = float8es, inetes = inetes, int2es = int2es, int2vectores = int2vectores, int4es = int4es, int8es = int8es, intervales = intervales, jsones = jsones, jsonbes = jsonbes, linees = linees, lseges = lseges, moneyes = moneyes, mydomaines = mydomaines, myenumes = myenumes, namees = namees, numerices = numerices, pathes = pathes, pointes = pointes, polygones = polygones, textes = textes, timees = timees, timestampes = timestampes, timestampzes = timestampzes, timezes = timezes, uuides = uuides, varchares = varchares, xmles = xmles)) + def publicTitle(code: TitleId = TitleId(random.alphanumeric.take(20).mkString)): ConnectionIO[TitleRow] = (new TitleRepoImpl).insert(new TitleRow(code = code)) + def publicTitleDomain(code: TitleDomainId = TitleDomainId(domainInsert.publicShortText(random))): ConnectionIO[TitleDomainRow] = (new TitleDomainRepoImpl).insert(new TitleDomainRow(code = code)) + def publicTitledperson(titleShort: TitleDomainId = TitleDomainId.All(random.nextInt(4)), + title: TitleId = TitleId.All(random.nextInt(4)), + name: String = random.alphanumeric.take(20).mkString + ): ConnectionIO[TitledpersonRow] = (new TitledpersonRepoImpl).insert(new TitledpersonRow(titleShort = titleShort, title = title, name = name)) def publicUsers(email: TypoUnknownCitext, userId: UsersId = UsersId(TypoUUID.randomUUID), name: String = random.alphanumeric.take(20).mkString, diff --git a/typo-tester-doobie/src/scala/adventureworks/ArrayTest.scala b/typo-tester-doobie/src/scala/adventureworks/ArrayTest.scala index b839081a70..66bff639a0 100644 --- a/typo-tester-doobie/src/scala/adventureworks/ArrayTest.scala +++ b/typo-tester-doobie/src/scala/adventureworks/ArrayTest.scala @@ -6,19 +6,12 @@ import adventureworks.public.pgtestnull.{PgtestnullRepoImpl, PgtestnullRow} import adventureworks.public.{Mydomain, Myenum} import cats.effect.IO import doobie.{ConnectionIO, WeakAsync} -import io.circe.Encoder -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.Assertion import org.scalatest.funsuite.AnyFunSuite -class ArrayTest extends AnyFunSuite with TypeCheckedTripleEquals { +class ArrayTest extends AnyFunSuite with JsonEquals { val pgtestnullRepo: PgtestnullRepoImpl = new PgtestnullRepoImpl val pgtestRepo: PgtestRepoImpl = new PgtestRepoImpl - // need to compare json instead of case classes because of arrays - def assertJsonEquals[A: Encoder](a1: A, a2: A): Assertion = - assert(Encoder[A].apply(a1) === Encoder[A].apply(a2)) - test("can insert pgtest rows") { withConnection { val before = ArrayTestData.pgTestRow diff --git a/typo-tester-doobie/src/scala/adventureworks/JsonEquals.scala b/typo-tester-doobie/src/scala/adventureworks/JsonEquals.scala new file mode 100644 index 0000000000..5bbfefde7b --- /dev/null +++ b/typo-tester-doobie/src/scala/adventureworks/JsonEquals.scala @@ -0,0 +1,12 @@ +package adventureworks + +import io.circe.Encoder +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.Assertion +import org.scalatest.funsuite.AnyFunSuite + +trait JsonEquals extends AnyFunSuite with TypeCheckedTripleEquals { + // need to compare json instead of case classes because of arrays + def assertJsonEquals[A: Encoder](a1: A, a2: A): Assertion = + assert(Encoder[A].apply(a1) === Encoder[A].apply(a2)) +} diff --git a/typo-tester-doobie/src/scala/adventureworks/OpenEnumTest.scala b/typo-tester-doobie/src/scala/adventureworks/OpenEnumTest.scala new file mode 100644 index 0000000000..2462f5b7f8 --- /dev/null +++ b/typo-tester-doobie/src/scala/adventureworks/OpenEnumTest.scala @@ -0,0 +1,35 @@ +package adventureworks + +import adventureworks.public.title.* +import adventureworks.public.title_domain.* +import adventureworks.public.titledperson.* +import org.scalatest.funsuite.AnyFunSuite +import typo.dsl.ToTupleOps + +import scala.util.Random + +class OpenEnumTest extends AnyFunSuite with JsonEquals { + val titleRepo = new TitleRepoImpl + val titleDomainRepo = new TitleDomainRepoImpl + val titledPersonRepo = new TitledpersonRepoImpl + val testInsert = new TestInsert(new Random(0), DomainInsert) + + test("works") { + withConnection { + for { + john <- testInsert.publicTitledperson(TitleDomainId.dr, TitleId.dr, "John") + found <- titledPersonRepo.select + .joinFk(_.fkTitle)(titleRepo.select.where(_.code.in(Array(TitleId.dr)))) + .joinFk(_._1.fkTitleDomain)(titleDomainRepo.select.where(_.code.in(Array(TitleDomainId.dr)))) + .where { case ((tp, _), _) => tp.name === "John" } + .toList + .map(_.headOption) + } yield { + val expected: Option[((TitledpersonRow, TitleRow), TitleDomainRow)] = + Option(john ~ TitleRow(TitleId.dr) ~ TitleDomainRow(TitleDomainId.dr)) + + assertJsonEquals(found, expected) + } + } + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleFields.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleFields.scala new file mode 100644 index 0000000000..a8ed6e549d --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleFields.scala @@ -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) + } + +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleId.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleId.scala new file mode 100644 index 0000000000..6134df20bd --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleId.scala @@ -0,0 +1,51 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import java.sql.Types +import typo.dsl.PGType +import zio.jdbc.JdbcDecoder +import zio.jdbc.JdbcEncoder +import zio.jdbc.SqlFragment.Setter +import zio.json.JsonDecoder +import zio.json.JsonEncoder + +/** 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 arrayJdbcDecoder: JdbcDecoder[Array[TitleId]] = adventureworks.StringArrayDecoder.map(a => if (a == null) null else a.map(apply)) + implicit lazy val arrayJdbcEncoder: JdbcEncoder[Array[TitleId]] = JdbcEncoder.singleParamEncoder(using arraySetter) + implicit lazy val arraySetter: Setter[Array[TitleId]] = adventureworks.StringArraySetter.contramap(_.map(_.value)) + implicit lazy val jdbcDecoder: JdbcDecoder[TitleId] = JdbcDecoder.stringDecoder.map(TitleId.apply) + implicit lazy val jdbcEncoder: JdbcEncoder[TitleId] = JdbcEncoder.stringEncoder.contramap(_.value) + implicit lazy val jsonDecoder: JsonDecoder[TitleId] = JsonDecoder.string.map(TitleId.apply) + implicit lazy val jsonEncoder: JsonEncoder[TitleId] = JsonEncoder.string.contramap(_.value) + implicit lazy val ordering: Ordering[TitleId] = Ordering.by(_.value) + implicit lazy val pgType: PGType[TitleId] = PGType.instance[TitleId]("text", Types.OTHER) + implicit lazy val setter: Setter[TitleId] = Setter.stringSetter.contramap(_.value) + 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) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala new file mode 100644 index 0000000000..918afbeca7 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepo.scala @@ -0,0 +1,34 @@ +/** + * 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.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder +import zio.ZIO +import zio.jdbc.UpdateResult +import zio.jdbc.ZConnection +import zio.stream.ZStream + +trait TitleRepo { + def delete: DeleteBuilder[TitleFields, TitleRow] + def deleteById(code: TitleId): ZIO[ZConnection, Throwable, Boolean] + def deleteByIds(codes: Array[TitleId]): ZIO[ZConnection, Throwable, Long] + def insert(unsaved: TitleRow): ZIO[ZConnection, Throwable, TitleRow] + def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] + def select: SelectBuilder[TitleFields, TitleRow] + def selectAll: ZStream[ZConnection, Throwable, TitleRow] + def selectById(code: TitleId): ZIO[ZConnection, Throwable, Option[TitleRow]] + def selectByIds(codes: Array[TitleId]): ZStream[ZConnection, Throwable, TitleRow] + def selectByIdsTracked(codes: Array[TitleId]): ZIO[ZConnection, Throwable, Map[TitleId, TitleRow]] + def update: UpdateBuilder[TitleFields, TitleRow] + def upsert(unsaved: TitleRow): ZIO[ZConnection, Throwable, UpdateResult[TitleRow]] + // Not implementable for zio-jdbc: upsertBatch + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + def upsertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala new file mode 100644 index 0000000000..efc033cc4c --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoImpl.scala @@ -0,0 +1,82 @@ +/** + * 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.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder +import zio.ZIO +import zio.jdbc.SqlFragment.Segment +import zio.jdbc.UpdateResult +import zio.jdbc.ZConnection +import zio.jdbc.sqlInterpolator +import zio.stream.ZStream + +class TitleRepoImpl extends TitleRepo { + override def delete: DeleteBuilder[TitleFields, TitleRow] = { + DeleteBuilder(""""public"."title"""", TitleFields.structure) + } + override def deleteById(code: TitleId): ZIO[ZConnection, Throwable, Boolean] = { + sql"""delete from "public"."title" where "code" = ${Segment.paramSegment(code)(TitleId.setter)}""".delete.map(_ > 0) + } + override def deleteByIds(codes: Array[TitleId]): ZIO[ZConnection, Throwable, Long] = { + sql"""delete from "public"."title" where "code" = ANY(${codes})""".delete + } + override def insert(unsaved: TitleRow): ZIO[ZConnection, Throwable, TitleRow] = { + sql"""insert into "public"."title"("code") + values (${Segment.paramSegment(unsaved.code)(TitleId.setter)}) + returning "code" + """.insertReturning(using TitleRow.jdbcDecoder).map(_.updatedKeys.head) + } + override def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + streamingInsert(s"""COPY "public"."title"("code") FROM STDIN""", batchSize, unsaved)(TitleRow.text) + } + override def select: SelectBuilder[TitleFields, TitleRow] = { + SelectBuilderSql(""""public"."title"""", TitleFields.structure, TitleRow.jdbcDecoder) + } + override def selectAll: ZStream[ZConnection, Throwable, TitleRow] = { + sql"""select "code" from "public"."title"""".query(using TitleRow.jdbcDecoder).selectStream() + } + override def selectById(code: TitleId): ZIO[ZConnection, Throwable, Option[TitleRow]] = { + sql"""select "code" from "public"."title" where "code" = ${Segment.paramSegment(code)(TitleId.setter)}""".query(using TitleRow.jdbcDecoder).selectOne + } + override def selectByIds(codes: Array[TitleId]): ZStream[ZConnection, Throwable, TitleRow] = { + sql"""select "code" from "public"."title" where "code" = ANY(${Segment.paramSegment(codes)(TitleId.arraySetter)})""".query(using TitleRow.jdbcDecoder).selectStream() + } + override def selectByIdsTracked(codes: Array[TitleId]): ZIO[ZConnection, Throwable, Map[TitleId, TitleRow]] = { + selectByIds(codes).runCollect.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleFields, TitleRow] = { + UpdateBuilder(""""public"."title"""", TitleFields.structure, TitleRow.jdbcDecoder) + } + override def upsert(unsaved: TitleRow): ZIO[ZConnection, Throwable, UpdateResult[TitleRow]] = { + sql"""insert into "public"."title"("code") + values ( + ${Segment.paramSegment(unsaved.code)(TitleId.setter)} + ) + on conflict ("code") + do nothing + returning "code"""".insertReturning(using TitleRow.jdbcDecoder) + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + val created = sql"""create temporary table title_TEMP (like "public"."title") on commit drop""".execute + val copied = streamingInsert(s"""copy title_TEMP("code") from stdin""", batchSize, unsaved)(TitleRow.text) + val merged = sql"""insert into "public"."title"("code") + select * from title_TEMP + on conflict ("code") + do nothing + ; + drop table title_TEMP;""".update + created *> copied *> merged + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala new file mode 100644 index 0000000000..3d277dbab9 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRepoMock.scala @@ -0,0 +1,91 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.DeleteBuilder.DeleteBuilderMock +import typo.dsl.DeleteParams +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderMock +import typo.dsl.SelectParams +import typo.dsl.UpdateBuilder +import typo.dsl.UpdateBuilder.UpdateBuilderMock +import typo.dsl.UpdateParams +import zio.Chunk +import zio.ZIO +import zio.jdbc.UpdateResult +import zio.jdbc.ZConnection +import zio.stream.ZStream + +class TitleRepoMock(map: scala.collection.mutable.Map[TitleId, TitleRow] = scala.collection.mutable.Map.empty) extends TitleRepo { + override def delete: DeleteBuilder[TitleFields, TitleRow] = { + DeleteBuilderMock(DeleteParams.empty, TitleFields.structure, map) + } + override def deleteById(code: TitleId): ZIO[ZConnection, Throwable, Boolean] = { + ZIO.succeed(map.remove(code).isDefined) + } + override def deleteByIds(codes: Array[TitleId]): ZIO[ZConnection, Throwable, Long] = { + ZIO.succeed(codes.map(id => map.remove(id)).count(_.isDefined).toLong) + } + override def insert(unsaved: TitleRow): ZIO[ZConnection, Throwable, TitleRow] = { + ZIO.succeed { + val _ = + if (map.contains(unsaved.code)) + sys.error(s"id ${unsaved.code} already exists") + else + map.put(unsaved.code, unsaved) + + unsaved + } + } + override def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + unsaved.scanZIO(0L) { case (acc, row) => + ZIO.succeed { + map += (row.code -> row) + acc + 1 + } + }.runLast.map(_.getOrElse(0L)) + } + override def select: SelectBuilder[TitleFields, TitleRow] = { + SelectBuilderMock(TitleFields.structure, ZIO.succeed(Chunk.fromIterable(map.values)), SelectParams.empty) + } + override def selectAll: ZStream[ZConnection, Throwable, TitleRow] = { + ZStream.fromIterable(map.values) + } + override def selectById(code: TitleId): ZIO[ZConnection, Throwable, Option[TitleRow]] = { + ZIO.succeed(map.get(code)) + } + override def selectByIds(codes: Array[TitleId]): ZStream[ZConnection, Throwable, TitleRow] = { + ZStream.fromIterable(codes.flatMap(map.get)) + } + override def selectByIdsTracked(codes: Array[TitleId]): ZIO[ZConnection, Throwable, Map[TitleId, TitleRow]] = { + selectByIds(codes).runCollect.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleFields, TitleRow] = { + UpdateBuilderMock(UpdateParams.empty, TitleFields.structure, map) + } + override def upsert(unsaved: TitleRow): ZIO[ZConnection, Throwable, UpdateResult[TitleRow]] = { + ZIO.succeed { + map.put(unsaved.code, unsaved): @nowarn + UpdateResult(1, Chunk.single(unsaved)) + } + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + unsaved.scanZIO(0L) { case (acc, row) => + ZIO.succeed { + map += (row.code -> row) + acc + 1 + } + }.runLast.map(_.getOrElse(0L)) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRow.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRow.scala new file mode 100644 index 0000000000..6c59bf8b60 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title/TitleRow.scala @@ -0,0 +1,43 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title + +import zio.jdbc.JdbcDecoder +import zio.json.JsonDecoder +import zio.json.JsonEncoder +import zio.json.ast.Json +import zio.json.internal.Write + +/** Table: public.title + Primary key: code */ +case class TitleRow( + code: TitleId +){ + val id = code + } + +object TitleRow { + implicit lazy val jdbcDecoder: JdbcDecoder[TitleRow] = TitleId.jdbcDecoder.map(v => TitleRow(code = v)) + implicit lazy val jsonDecoder: JsonDecoder[TitleRow] = JsonDecoder[Json.Obj].mapOrFail { jsonObj => + val code = jsonObj.get("code").toRight("Missing field 'code'").flatMap(_.as(TitleId.jsonDecoder)) + if (code.isRight) + Right(TitleRow(code = code.toOption.get)) + else Left(List[Either[String, Any]](code).flatMap(_.left.toOption).mkString(", ")) + } + implicit lazy val jsonEncoder: JsonEncoder[TitleRow] = new JsonEncoder[TitleRow] { + override def unsafeEncode(a: TitleRow, indent: Option[Int], out: Write): Unit = { + out.write("{") + out.write(""""code":""") + TitleId.jsonEncoder.unsafeEncode(a.code, indent, out) + out.write("}") + } + } + implicit lazy val text: Text[TitleRow] = Text.instance[TitleRow]{ (row, sb) => + TitleId.text.unsafeEncode(row.code, sb) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala new file mode 100644 index 0000000000..c87f1bab4b --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainFields.scala @@ -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_domain + +import typo.dsl.Path +import typo.dsl.SqlExpr.FieldLikeNoHkt +import typo.dsl.SqlExpr.IdField +import typo.dsl.Structure.Relation + +trait TitleDomainFields { + def code: IdField[TitleDomainId, TitleDomainRow] +} + +object TitleDomainFields { + lazy val structure: Relation[TitleDomainFields, TitleDomainRow] = + new Impl(Nil) + + private final class Impl(val _path: List[Path]) + extends Relation[TitleDomainFields, TitleDomainRow] { + + override lazy val fields: TitleDomainFields = new TitleDomainFields { + override def code = IdField[TitleDomainId, TitleDomainRow](_path, "code", None, Some("text"), x => x.code, (row, value) => row.copy(code = value)) + } + + override lazy val columns: List[FieldLikeNoHkt[?, TitleDomainRow]] = + List[FieldLikeNoHkt[?, TitleDomainRow]](fields.code) + + override def copy(path: List[Path]): Impl = + new Impl(path) + } + +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala new file mode 100644 index 0000000000..03a39d0859 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainId.scala @@ -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_domain + +import java.sql.Types +import typo.dsl.PGType +import zio.jdbc.JdbcDecoder +import zio.jdbc.JdbcEncoder +import zio.jdbc.SqlFragment.Setter +import zio.json.JsonDecoder +import zio.json.JsonEncoder + +/** Type for the primary key of table `public.title_domain`. It has some known values: + * - dr + * - mr + * - ms + * - phd + */ +sealed abstract class TitleDomainId(val value: ShortText) + +object TitleDomainId { + def apply(underlying: ShortText): TitleDomainId = + ByName.getOrElse(underlying, Unknown(underlying)) + def shortText(value: String): TitleDomainId = TitleDomainId(ShortText(value)) + case object dr extends TitleDomainId(ShortText("dr")) + case object mr extends TitleDomainId(ShortText("mr")) + case object ms extends TitleDomainId(ShortText("ms")) + case object phd extends TitleDomainId(ShortText("phd")) + case class Unknown(override val value: ShortText) extends TitleDomainId(value) + val All: List[TitleDomainId] = List(dr, mr, ms, phd) + val ByName: Map[ShortText, TitleDomainId] = All.map(x => (x.value, x)).toMap + + implicit lazy val arrayJdbcDecoder: JdbcDecoder[Array[TitleDomainId]] = JdbcDecoder[Array[ShortText]].map(a => if (a == null) null else a.map(apply)) + implicit lazy val arrayJdbcEncoder: JdbcEncoder[Array[TitleDomainId]] = JdbcEncoder.singleParamEncoder(using arraySetter) + implicit lazy val arraySetter: Setter[Array[TitleDomainId]] = ShortText.arraySetter.contramap(_.map(_.value)) + implicit lazy val jdbcDecoder: JdbcDecoder[TitleDomainId] = ShortText.jdbcDecoder.map(TitleDomainId.apply) + implicit lazy val jdbcEncoder: JdbcEncoder[TitleDomainId] = ShortText.jdbcEncoder.contramap(_.value) + implicit lazy val jsonDecoder: JsonDecoder[TitleDomainId] = ShortText.jsonDecoder.map(TitleDomainId.apply) + implicit lazy val jsonEncoder: JsonEncoder[TitleDomainId] = ShortText.jsonEncoder.contramap(_.value) + implicit lazy val ordering: Ordering[TitleDomainId] = Ordering.by(_.value) + implicit lazy val pgType: PGType[TitleDomainId] = PGType.instance[TitleDomainId](""""public"."short_text"""", Types.OTHER) + implicit lazy val setter: Setter[TitleDomainId] = ShortText.setter.contramap(_.value) + implicit lazy val text: Text[TitleDomainId] = new Text[TitleDomainId] { + override def unsafeEncode(v: TitleDomainId, sb: StringBuilder) = ShortText.text.unsafeEncode(v.value, sb) + override def unsafeArrayEncode(v: TitleDomainId, sb: StringBuilder) = ShortText.text.unsafeArrayEncode(v.value, sb) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala new file mode 100644 index 0000000000..b3ef5280df --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepo.scala @@ -0,0 +1,34 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder +import zio.ZIO +import zio.jdbc.UpdateResult +import zio.jdbc.ZConnection +import zio.stream.ZStream + +trait TitleDomainRepo { + def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] + def deleteById(code: TitleDomainId): ZIO[ZConnection, Throwable, Boolean] + def deleteByIds(codes: Array[TitleDomainId]): ZIO[ZConnection, Throwable, Long] + def insert(unsaved: TitleDomainRow): ZIO[ZConnection, Throwable, TitleDomainRow] + def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleDomainRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] + def select: SelectBuilder[TitleDomainFields, TitleDomainRow] + def selectAll: ZStream[ZConnection, Throwable, TitleDomainRow] + def selectById(code: TitleDomainId): ZIO[ZConnection, Throwable, Option[TitleDomainRow]] + def selectByIds(codes: Array[TitleDomainId]): ZStream[ZConnection, Throwable, TitleDomainRow] + def selectByIdsTracked(codes: Array[TitleDomainId]): ZIO[ZConnection, Throwable, Map[TitleDomainId, TitleDomainRow]] + def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] + def upsert(unsaved: TitleDomainRow): ZIO[ZConnection, Throwable, UpdateResult[TitleDomainRow]] + // Not implementable for zio-jdbc: upsertBatch + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + def upsertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleDomainRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala new file mode 100644 index 0000000000..41a00f3c22 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoImpl.scala @@ -0,0 +1,82 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder +import zio.ZIO +import zio.jdbc.SqlFragment.Segment +import zio.jdbc.UpdateResult +import zio.jdbc.ZConnection +import zio.jdbc.sqlInterpolator +import zio.stream.ZStream + +class TitleDomainRepoImpl extends TitleDomainRepo { + override def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] = { + DeleteBuilder(""""public"."title_domain"""", TitleDomainFields.structure) + } + override def deleteById(code: TitleDomainId): ZIO[ZConnection, Throwable, Boolean] = { + sql"""delete from "public"."title_domain" where "code" = ${Segment.paramSegment(code)(TitleDomainId.setter)}""".delete.map(_ > 0) + } + override def deleteByIds(codes: Array[TitleDomainId]): ZIO[ZConnection, Throwable, Long] = { + sql"""delete from "public"."title_domain" where "code" = ANY(${codes})""".delete + } + override def insert(unsaved: TitleDomainRow): ZIO[ZConnection, Throwable, TitleDomainRow] = { + sql"""insert into "public"."title_domain"("code") + values (${Segment.paramSegment(unsaved.code)(TitleDomainId.setter)}::text) + returning "code" + """.insertReturning(using TitleDomainRow.jdbcDecoder).map(_.updatedKeys.head) + } + override def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleDomainRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + streamingInsert(s"""COPY "public"."title_domain"("code") FROM STDIN""", batchSize, unsaved)(TitleDomainRow.text) + } + override def select: SelectBuilder[TitleDomainFields, TitleDomainRow] = { + SelectBuilderSql(""""public"."title_domain"""", TitleDomainFields.structure, TitleDomainRow.jdbcDecoder) + } + override def selectAll: ZStream[ZConnection, Throwable, TitleDomainRow] = { + sql"""select "code" from "public"."title_domain"""".query(using TitleDomainRow.jdbcDecoder).selectStream() + } + override def selectById(code: TitleDomainId): ZIO[ZConnection, Throwable, Option[TitleDomainRow]] = { + sql"""select "code" from "public"."title_domain" where "code" = ${Segment.paramSegment(code)(TitleDomainId.setter)}""".query(using TitleDomainRow.jdbcDecoder).selectOne + } + override def selectByIds(codes: Array[TitleDomainId]): ZStream[ZConnection, Throwable, TitleDomainRow] = { + sql"""select "code" from "public"."title_domain" where "code" = ANY(${Segment.paramSegment(codes)(TitleDomainId.arraySetter)})""".query(using TitleDomainRow.jdbcDecoder).selectStream() + } + override def selectByIdsTracked(codes: Array[TitleDomainId]): ZIO[ZConnection, Throwable, Map[TitleDomainId, TitleDomainRow]] = { + selectByIds(codes).runCollect.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] = { + UpdateBuilder(""""public"."title_domain"""", TitleDomainFields.structure, TitleDomainRow.jdbcDecoder) + } + override def upsert(unsaved: TitleDomainRow): ZIO[ZConnection, Throwable, UpdateResult[TitleDomainRow]] = { + sql"""insert into "public"."title_domain"("code") + values ( + ${Segment.paramSegment(unsaved.code)(TitleDomainId.setter)}::text + ) + on conflict ("code") + do nothing + returning "code"""".insertReturning(using TitleDomainRow.jdbcDecoder) + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleDomainRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + val created = sql"""create temporary table title_domain_TEMP (like "public"."title_domain") on commit drop""".execute + val copied = streamingInsert(s"""copy title_domain_TEMP("code") from stdin""", batchSize, unsaved)(TitleDomainRow.text) + val merged = sql"""insert into "public"."title_domain"("code") + select * from title_domain_TEMP + on conflict ("code") + do nothing + ; + drop table title_domain_TEMP;""".update + created *> copied *> merged + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala new file mode 100644 index 0000000000..8ed7929aac --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRepoMock.scala @@ -0,0 +1,91 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import scala.annotation.nowarn +import typo.dsl.DeleteBuilder +import typo.dsl.DeleteBuilder.DeleteBuilderMock +import typo.dsl.DeleteParams +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderMock +import typo.dsl.SelectParams +import typo.dsl.UpdateBuilder +import typo.dsl.UpdateBuilder.UpdateBuilderMock +import typo.dsl.UpdateParams +import zio.Chunk +import zio.ZIO +import zio.jdbc.UpdateResult +import zio.jdbc.ZConnection +import zio.stream.ZStream + +class TitleDomainRepoMock(map: scala.collection.mutable.Map[TitleDomainId, TitleDomainRow] = scala.collection.mutable.Map.empty) extends TitleDomainRepo { + override def delete: DeleteBuilder[TitleDomainFields, TitleDomainRow] = { + DeleteBuilderMock(DeleteParams.empty, TitleDomainFields.structure, map) + } + override def deleteById(code: TitleDomainId): ZIO[ZConnection, Throwable, Boolean] = { + ZIO.succeed(map.remove(code).isDefined) + } + override def deleteByIds(codes: Array[TitleDomainId]): ZIO[ZConnection, Throwable, Long] = { + ZIO.succeed(codes.map(id => map.remove(id)).count(_.isDefined).toLong) + } + override def insert(unsaved: TitleDomainRow): ZIO[ZConnection, Throwable, TitleDomainRow] = { + ZIO.succeed { + val _ = + if (map.contains(unsaved.code)) + sys.error(s"id ${unsaved.code} already exists") + else + map.put(unsaved.code, unsaved) + + unsaved + } + } + override def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleDomainRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + unsaved.scanZIO(0L) { case (acc, row) => + ZIO.succeed { + map += (row.code -> row) + acc + 1 + } + }.runLast.map(_.getOrElse(0L)) + } + override def select: SelectBuilder[TitleDomainFields, TitleDomainRow] = { + SelectBuilderMock(TitleDomainFields.structure, ZIO.succeed(Chunk.fromIterable(map.values)), SelectParams.empty) + } + override def selectAll: ZStream[ZConnection, Throwable, TitleDomainRow] = { + ZStream.fromIterable(map.values) + } + override def selectById(code: TitleDomainId): ZIO[ZConnection, Throwable, Option[TitleDomainRow]] = { + ZIO.succeed(map.get(code)) + } + override def selectByIds(codes: Array[TitleDomainId]): ZStream[ZConnection, Throwable, TitleDomainRow] = { + ZStream.fromIterable(codes.flatMap(map.get)) + } + override def selectByIdsTracked(codes: Array[TitleDomainId]): ZIO[ZConnection, Throwable, Map[TitleDomainId, TitleDomainRow]] = { + selectByIds(codes).runCollect.map { rows => + val byId = rows.view.map(x => (x.code, x)).toMap + codes.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap + } + } + override def update: UpdateBuilder[TitleDomainFields, TitleDomainRow] = { + UpdateBuilderMock(UpdateParams.empty, TitleDomainFields.structure, map) + } + override def upsert(unsaved: TitleDomainRow): ZIO[ZConnection, Throwable, UpdateResult[TitleDomainRow]] = { + ZIO.succeed { + map.put(unsaved.code, unsaved): @nowarn + UpdateResult(1, Chunk.single(unsaved)) + } + } + /* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */ + override def upsertStreaming(unsaved: ZStream[ZConnection, Throwable, TitleDomainRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + unsaved.scanZIO(0L) { case (acc, row) => + ZIO.succeed { + map += (row.code -> row) + acc + 1 + } + }.runLast.map(_.getOrElse(0L)) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala new file mode 100644 index 0000000000..0becbaac07 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/title_domain/TitleDomainRow.scala @@ -0,0 +1,43 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package title_domain + +import zio.jdbc.JdbcDecoder +import zio.json.JsonDecoder +import zio.json.JsonEncoder +import zio.json.ast.Json +import zio.json.internal.Write + +/** Table: public.title_domain + Primary key: code */ +case class TitleDomainRow( + code: TitleDomainId +){ + val id = code + } + +object TitleDomainRow { + implicit lazy val jdbcDecoder: JdbcDecoder[TitleDomainRow] = TitleDomainId.jdbcDecoder.map(v => TitleDomainRow(code = v)) + implicit lazy val jsonDecoder: JsonDecoder[TitleDomainRow] = JsonDecoder[Json.Obj].mapOrFail { jsonObj => + val code = jsonObj.get("code").toRight("Missing field 'code'").flatMap(_.as(TitleDomainId.jsonDecoder)) + if (code.isRight) + Right(TitleDomainRow(code = code.toOption.get)) + else Left(List[Either[String, Any]](code).flatMap(_.left.toOption).mkString(", ")) + } + implicit lazy val jsonEncoder: JsonEncoder[TitleDomainRow] = new JsonEncoder[TitleDomainRow] { + override def unsafeEncode(a: TitleDomainRow, indent: Option[Int], out: Write): Unit = { + out.write("{") + out.write(""""code":""") + TitleDomainId.jsonEncoder.unsafeEncode(a.code, indent, out) + out.write("}") + } + } + implicit lazy val text: Text[TitleDomainRow] = Text.instance[TitleDomainRow]{ (row, sb) => + TitleDomainId.text.unsafeEncode(row.code, sb) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala new file mode 100644 index 0000000000..5b42488ef4 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonFields.scala @@ -0,0 +1,54 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleFields +import adventureworks.public.title.TitleId +import adventureworks.public.title.TitleRow +import adventureworks.public.title_domain.TitleDomainFields +import adventureworks.public.title_domain.TitleDomainId +import adventureworks.public.title_domain.TitleDomainRow +import typo.dsl.ForeignKey +import typo.dsl.Path +import typo.dsl.SqlExpr.Field +import typo.dsl.SqlExpr.FieldLikeNoHkt +import typo.dsl.Structure.Relation + +trait TitledpersonFields { + def titleShort: Field[TitleDomainId, TitledpersonRow] + def title: Field[TitleId, TitledpersonRow] + def name: Field[String, TitledpersonRow] + def fkTitle: ForeignKey[TitleFields, TitleRow] = + ForeignKey[TitleFields, TitleRow]("public.titledperson_title_fkey", Nil) + .withColumnPair(title, _.code) + def fkTitleDomain: ForeignKey[TitleDomainFields, TitleDomainRow] = + ForeignKey[TitleDomainFields, TitleDomainRow]("public.titledperson_title_short_fkey", Nil) + .withColumnPair(titleShort, _.code) +} + +object TitledpersonFields { + lazy val structure: Relation[TitledpersonFields, TitledpersonRow] = + new Impl(Nil) + + private final class Impl(val _path: List[Path]) + extends Relation[TitledpersonFields, TitledpersonRow] { + + override lazy val fields: TitledpersonFields = new TitledpersonFields { + override def titleShort = Field[TitleDomainId, TitledpersonRow](_path, "title_short", None, Some("text"), x => x.titleShort, (row, value) => row.copy(titleShort = value)) + override def title = Field[TitleId, TitledpersonRow](_path, "title", None, None, x => x.title, (row, value) => row.copy(title = value)) + override def name = Field[String, TitledpersonRow](_path, "name", None, None, x => x.name, (row, value) => row.copy(name = value)) + } + + override lazy val columns: List[FieldLikeNoHkt[?, TitledpersonRow]] = + List[FieldLikeNoHkt[?, TitledpersonRow]](fields.titleShort, fields.title, fields.name) + + override def copy(path: List[Path]): Impl = + new Impl(path) + } + +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala new file mode 100644 index 0000000000..2055785408 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepo.scala @@ -0,0 +1,24 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.UpdateBuilder +import zio.ZIO +import zio.jdbc.ZConnection +import zio.stream.ZStream + +trait TitledpersonRepo { + def delete: DeleteBuilder[TitledpersonFields, TitledpersonRow] + def insert(unsaved: TitledpersonRow): ZIO[ZConnection, Throwable, TitledpersonRow] + def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitledpersonRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] + def select: SelectBuilder[TitledpersonFields, TitledpersonRow] + def selectAll: ZStream[ZConnection, Throwable, TitledpersonRow] + def update: UpdateBuilder[TitledpersonFields, TitledpersonRow] +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala new file mode 100644 index 0000000000..39a56b2e31 --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRepoImpl.scala @@ -0,0 +1,45 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleId +import adventureworks.public.title_domain.TitleDomainId +import typo.dsl.DeleteBuilder +import typo.dsl.SelectBuilder +import typo.dsl.SelectBuilderSql +import typo.dsl.UpdateBuilder +import zio.ZIO +import zio.jdbc.SqlFragment.Segment +import zio.jdbc.SqlFragment.Setter +import zio.jdbc.ZConnection +import zio.jdbc.sqlInterpolator +import zio.stream.ZStream + +class TitledpersonRepoImpl extends TitledpersonRepo { + override def delete: DeleteBuilder[TitledpersonFields, TitledpersonRow] = { + DeleteBuilder(""""public"."titledperson"""", TitledpersonFields.structure) + } + override def insert(unsaved: TitledpersonRow): ZIO[ZConnection, Throwable, TitledpersonRow] = { + sql"""insert into "public"."titledperson"("title_short", "title", "name") + values (${Segment.paramSegment(unsaved.titleShort)(TitleDomainId.setter)}::text, ${Segment.paramSegment(unsaved.title)(TitleId.setter)}, ${Segment.paramSegment(unsaved.name)(Setter.stringSetter)}) + returning "title_short", "title", "name" + """.insertReturning(using TitledpersonRow.jdbcDecoder).map(_.updatedKeys.head) + } + override def insertStreaming(unsaved: ZStream[ZConnection, Throwable, TitledpersonRow], batchSize: Int = 10000): ZIO[ZConnection, Throwable, Long] = { + streamingInsert(s"""COPY "public"."titledperson"("title_short", "title", "name") FROM STDIN""", batchSize, unsaved)(TitledpersonRow.text) + } + override def select: SelectBuilder[TitledpersonFields, TitledpersonRow] = { + SelectBuilderSql(""""public"."titledperson"""", TitledpersonFields.structure, TitledpersonRow.jdbcDecoder) + } + override def selectAll: ZStream[ZConnection, Throwable, TitledpersonRow] = { + sql"""select "title_short", "title", "name" from "public"."titledperson"""".query(using TitledpersonRow.jdbcDecoder).selectStream() + } + override def update: UpdateBuilder[TitledpersonFields, TitledpersonRow] = { + UpdateBuilder(""""public"."titledperson"""", TitledpersonFields.structure, TitledpersonRow.jdbcDecoder) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala new file mode 100644 index 0000000000..59b1f1135b --- /dev/null +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/public/titledperson/TitledpersonRow.scala @@ -0,0 +1,67 @@ +/** + * File has been automatically generated by `typo`. + * + * IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN. + */ +package adventureworks +package public +package titledperson + +import adventureworks.public.title.TitleId +import adventureworks.public.title_domain.TitleDomainId +import java.sql.ResultSet +import zio.jdbc.JdbcDecoder +import zio.json.JsonDecoder +import zio.json.JsonEncoder +import zio.json.ast.Json +import zio.json.internal.Write + +/** Table: public.titledperson */ +case class TitledpersonRow( + /** Points to [[title_domain.TitleDomainRow.code]] */ + titleShort: TitleDomainId, + /** Points to [[title.TitleRow.code]] */ + title: TitleId, + name: String +) + +object TitledpersonRow { + implicit lazy val jdbcDecoder: JdbcDecoder[TitledpersonRow] = new JdbcDecoder[TitledpersonRow] { + override def unsafeDecode(columIndex: Int, rs: ResultSet): (Int, TitledpersonRow) = + columIndex + 2 -> + TitledpersonRow( + titleShort = TitleDomainId.jdbcDecoder.unsafeDecode(columIndex + 0, rs)._2, + title = TitleId.jdbcDecoder.unsafeDecode(columIndex + 1, rs)._2, + name = JdbcDecoder.stringDecoder.unsafeDecode(columIndex + 2, rs)._2 + ) + } + implicit lazy val jsonDecoder: JsonDecoder[TitledpersonRow] = JsonDecoder[Json.Obj].mapOrFail { jsonObj => + val titleShort = jsonObj.get("title_short").toRight("Missing field 'title_short'").flatMap(_.as(TitleDomainId.jsonDecoder)) + val title = jsonObj.get("title").toRight("Missing field 'title'").flatMap(_.as(TitleId.jsonDecoder)) + val name = jsonObj.get("name").toRight("Missing field 'name'").flatMap(_.as(JsonDecoder.string)) + if (titleShort.isRight && title.isRight && name.isRight) + Right(TitledpersonRow(titleShort = titleShort.toOption.get, title = title.toOption.get, name = name.toOption.get)) + else Left(List[Either[String, Any]](titleShort, title, name).flatMap(_.left.toOption).mkString(", ")) + } + implicit lazy val jsonEncoder: JsonEncoder[TitledpersonRow] = new JsonEncoder[TitledpersonRow] { + override def unsafeEncode(a: TitledpersonRow, indent: Option[Int], out: Write): Unit = { + out.write("{") + out.write(""""title_short":""") + TitleDomainId.jsonEncoder.unsafeEncode(a.titleShort, indent, out) + out.write(",") + out.write(""""title":""") + TitleId.jsonEncoder.unsafeEncode(a.title, indent, out) + out.write(",") + out.write(""""name":""") + JsonEncoder.string.unsafeEncode(a.name, indent, out) + out.write("}") + } + } + implicit lazy val text: Text[TitledpersonRow] = Text.instance[TitledpersonRow]{ (row, sb) => + TitleDomainId.text.unsafeEncode(row.titleShort, sb) + sb.append(Text.DELIMETER) + TitleId.text.unsafeEncode(row.title, sb) + sb.append(Text.DELIMETER) + Text.stringInstance.unsafeEncode(row.name, sb) + } +} diff --git a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/testInsert.scala b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/testInsert.scala index 111f3b764f..d38825a47a 100644 --- a/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/testInsert.scala +++ b/typo-tester-zio-jdbc/generated-and-checked-in/adventureworks/testInsert.scala @@ -208,6 +208,14 @@ import adventureworks.public.pgtest.PgtestRepoImpl import adventureworks.public.pgtest.PgtestRow import adventureworks.public.pgtestnull.PgtestnullRepoImpl import adventureworks.public.pgtestnull.PgtestnullRow +import adventureworks.public.title.TitleId +import adventureworks.public.title.TitleRepoImpl +import adventureworks.public.title.TitleRow +import adventureworks.public.title_domain.TitleDomainId +import adventureworks.public.title_domain.TitleDomainRepoImpl +import adventureworks.public.title_domain.TitleDomainRow +import adventureworks.public.titledperson.TitledpersonRepoImpl +import adventureworks.public.titledperson.TitledpersonRow import adventureworks.public.users.UsersId import adventureworks.public.users.UsersRepoImpl import adventureworks.public.users.UsersRow @@ -774,6 +782,12 @@ class TestInsert(random: Random, domainInsert: TestDomainInsert) { varchares: Option[Array[String]] = if (random.nextBoolean()) None else Some(Array.fill(random.nextInt(3))(random.alphanumeric.take(20).mkString)), xmles: Option[Array[TypoXml]] = None ): ZIO[ZConnection, Throwable, PgtestnullRow] = (new PgtestnullRepoImpl).insert(new PgtestnullRow(bool = bool, box = box, bpchar = bpchar, bytea = bytea, char = char, circle = circle, date = date, float4 = float4, float8 = float8, hstore = hstore, inet = inet, int2 = int2, int2vector = int2vector, int4 = int4, int8 = int8, interval = interval, json = json, jsonb = jsonb, line = line, lseg = lseg, money = money, mydomain = mydomain, myenum = myenum, name = name, numeric = numeric, path = path, point = point, polygon = polygon, text = text, time = time, timestamp = timestamp, timestampz = timestampz, timez = timez, uuid = uuid, varchar = varchar, vector = vector, xml = xml, boxes = boxes, bpchares = bpchares, chares = chares, circlees = circlees, datees = datees, float4es = float4es, float8es = float8es, inetes = inetes, int2es = int2es, int2vectores = int2vectores, int4es = int4es, int8es = int8es, intervales = intervales, jsones = jsones, jsonbes = jsonbes, linees = linees, lseges = lseges, moneyes = moneyes, mydomaines = mydomaines, myenumes = myenumes, namees = namees, numerices = numerices, pathes = pathes, pointes = pointes, polygones = polygones, textes = textes, timees = timees, timestampes = timestampes, timestampzes = timestampzes, timezes = timezes, uuides = uuides, varchares = varchares, xmles = xmles)) + def publicTitle(code: TitleId = TitleId(random.alphanumeric.take(20).mkString)): ZIO[ZConnection, Throwable, TitleRow] = (new TitleRepoImpl).insert(new TitleRow(code = code)) + def publicTitleDomain(code: TitleDomainId = TitleDomainId(domainInsert.publicShortText(random))): ZIO[ZConnection, Throwable, TitleDomainRow] = (new TitleDomainRepoImpl).insert(new TitleDomainRow(code = code)) + def publicTitledperson(titleShort: TitleDomainId = TitleDomainId.All(random.nextInt(4)), + title: TitleId = TitleId.All(random.nextInt(4)), + name: String = random.alphanumeric.take(20).mkString + ): ZIO[ZConnection, Throwable, TitledpersonRow] = (new TitledpersonRepoImpl).insert(new TitledpersonRow(titleShort = titleShort, title = title, name = name)) def publicUsers(email: TypoUnknownCitext, userId: UsersId = UsersId(TypoUUID.randomUUID), name: String = random.alphanumeric.take(20).mkString, diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/ArrayTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/ArrayTest.scala index ad2025f2f7..93e41a5cdd 100644 --- a/typo-tester-zio-jdbc/src/scala/adventureworks/ArrayTest.scala +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/ArrayTest.scala @@ -4,21 +4,14 @@ import adventureworks.customtypes.* import adventureworks.public.pgtest.{PgtestRepoImpl, PgtestRow} import adventureworks.public.pgtestnull.{PgtestnullRepoImpl, PgtestnullRow} import adventureworks.public.{Mydomain, Myenum} -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.Assertion import org.scalatest.funsuite.AnyFunSuite import zio.Chunk -import zio.json.JsonEncoder import zio.stream.ZStream -class ArrayTest extends AnyFunSuite with TypeCheckedTripleEquals { +class ArrayTest extends AnyFunSuite with JsonEquals { val pgtestnullRepo: PgtestnullRepoImpl = new PgtestnullRepoImpl val pgtestRepo: PgtestRepoImpl = new PgtestRepoImpl - // need to compare json instead of case classes because of arrays - def assertJsonEquals[A: JsonEncoder](a1: A, a2: A): Assertion = - assert(JsonEncoder[A].toJsonAST(a1) === JsonEncoder[A].toJsonAST(a2)) - test("can insert pgtest rows") { withConnection { val before = ArrayTestData.pgTestRow diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/JsonEquals.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/JsonEquals.scala new file mode 100644 index 0000000000..127562393b --- /dev/null +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/JsonEquals.scala @@ -0,0 +1,12 @@ +package adventureworks + +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.Assertion +import org.scalatest.funsuite.AnyFunSuite +import zio.json.JsonEncoder + +trait JsonEquals extends AnyFunSuite with TypeCheckedTripleEquals { + // need to compare json instead of case classes because of arrays + def assertJsonEquals[A: JsonEncoder](a1: A, a2: A): Assertion = + assert(JsonEncoder[A].toJsonAST(a1) === JsonEncoder[A].toJsonAST(a2)) +} diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/OpenEnumTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/OpenEnumTest.scala new file mode 100644 index 0000000000..7a05d6aaba --- /dev/null +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/OpenEnumTest.scala @@ -0,0 +1,35 @@ +package adventureworks + +import adventureworks.public.title.* +import adventureworks.public.title_domain.* +import adventureworks.public.titledperson.* +import org.scalatest.funsuite.AnyFunSuite +import typo.dsl.ToTupleOps + +import scala.util.Random + +class OpenEnumTest extends AnyFunSuite with JsonEquals { + val titleRepo = new TitleRepoImpl + val titleDomainRepo = new TitleDomainRepoImpl + val titledPersonRepo = new TitledpersonRepoImpl + val testInsert = new TestInsert(new Random(0), DomainInsert) + + test("works") { + withConnection { + for { + john <- testInsert.publicTitledperson(TitleDomainId.dr, TitleId.dr, "John") + found <- titledPersonRepo.select + .joinFk(_.fkTitle)(titleRepo.select.where(_.code.in(Array(TitleId.dr)))) + .joinFk(_._1.fkTitleDomain)(titleDomainRepo.select.where(_.code.in(Array(TitleDomainId.dr)))) + .where { case ((tp, _), _) => tp.name === "John" } + .toChunk + .map(_.headOption) + } yield { + val expected: Option[((TitledpersonRow, TitleRow), TitleDomainRow)] = + Option(john ~ TitleRow(TitleId.dr) ~ TitleDomainRow(TitleDomainId.dr)) + + assertJsonEquals(found, expected) + } + } + } +} diff --git a/typo/src/scala/typo/Options.scala b/typo/src/scala/typo/Options.scala index ae4accc8cf..bccc9e6a23 100644 --- a/typo/src/scala/typo/Options.scala +++ b/typo/src/scala/typo/Options.scala @@ -25,7 +25,8 @@ case class Options( keepDependencies: Boolean = false, rewriteDatabase: MetaDb => MetaDb = identity, executionContext: ExecutionContext = ExecutionContext.global, - schemaMode: SchemaMode = SchemaMode.MultiSchema + schemaMode: SchemaMode = SchemaMode.MultiSchema, + openEnums: Selector = Selector.None ) object Options { diff --git a/typo/src/scala/typo/db.scala b/typo/src/scala/typo/db.scala index 39b50f5be7..2fd1b6a31e 100644 --- a/typo/src/scala/typo/db.scala +++ b/typo/src/scala/typo/db.scala @@ -56,7 +56,7 @@ object db { case object regrole extends Type case object regtype extends Type case object xid extends Type - case class EnumRef(name: RelationName) extends Type + case class EnumRef(enm: StringEnum) extends Type case object Text extends Type case object Time extends Type case object TimeTz extends Type @@ -70,7 +70,7 @@ object db { } case class Domain(name: RelationName, tpe: Type, originalType: String, isNotNull: Nullability, hasDefault: Boolean, constraintDefinition: Option[String]) - case class StringEnum(name: RelationName, values: List[String]) + case class StringEnum(name: RelationName, values: NonEmptyList[String]) case class ColName(value: String) extends AnyVal object ColName { implicit val ordering: Ordering[ColName] = Ordering.by(_.value) diff --git a/typo/src/scala/typo/generateFromDb.scala b/typo/src/scala/typo/generateFromDb.scala index ff722283a9..68082d5edf 100644 --- a/typo/src/scala/typo/generateFromDb.scala +++ b/typo/src/scala/typo/generateFromDb.scala @@ -1,5 +1,6 @@ package typo +import typo.internal.metadb.OpenEnum import typo.internal.sqlfiles.readSqlFileDirectories import java.nio.file.Path @@ -34,10 +35,13 @@ object generateFromDb { val viewSelector = graph.toList.map(_.value).foldLeft(Selector.None)(_.or(_)) val eventualMetaDb = MetaDb.fromDb(options.logger, dataSource, viewSelector, options.schemaMode) val eventualScripts = graph.mapScripts(paths => Future.sequence(paths.map(p => readSqlFileDirectories(options.logger, p, dataSource))).map(_.flatten)) + val combined = for { metaDb <- eventualMetaDb + eventualOpenEnums = OpenEnum.find(dataSource, options.logger, viewSelector, openEnumSelector = options.openEnums, metaDb = metaDb) + openEnums <- eventualOpenEnums scripts <- eventualScripts - } yield internal.generate(options, metaDb, scripts) + } yield internal.generate(options, metaDb, scripts, openEnums) Await.result(combined, Duration.Inf) } diff --git a/typo/src/scala/typo/internal/ComputedStringEnum.scala b/typo/src/scala/typo/internal/ComputedStringEnum.scala index 4601b91809..cfc4c22f8b 100644 --- a/typo/src/scala/typo/internal/ComputedStringEnum.scala +++ b/typo/src/scala/typo/internal/ComputedStringEnum.scala @@ -1,20 +1,17 @@ -package typo.internal - -import typo.{Naming, db, sc} +package typo +package internal case class ComputedStringEnum( dbEnum: db.StringEnum, tpe: sc.Type.Qualified, - name: db.RelationName, - members: List[(sc.Ident, String)] + members: NonEmptyList[(sc.Ident, String)] ) - object ComputedStringEnum { def apply(naming: Naming)(enm: db.StringEnum): ComputedStringEnum = new ComputedStringEnum( enm, sc.Type.Qualified(naming.enumName(enm.name)), - enm.name, enm.values.map { value => naming.enumValue(value) -> value } ) + } diff --git a/typo/src/scala/typo/internal/ComputedTable.scala b/typo/src/scala/typo/internal/ComputedTable.scala index 37823bb5d9..d64379be7e 100644 --- a/typo/src/scala/typo/internal/ComputedTable.scala +++ b/typo/src/scala/typo/internal/ComputedTable.scala @@ -1,6 +1,7 @@ package typo package internal +import typo.internal.metadb.OpenEnum import typo.internal.rewriteDependentData.Eval case class ComputedTable( @@ -9,7 +10,8 @@ case class ComputedTable( dbTable: db.Table, naming: Naming, scalaTypeMapper: TypeMapperScala, - eval: Eval[db.RelationName, HasSource] + eval: Eval[db.RelationName, HasSource], + openEnumsByTable: Map[db.RelationName, OpenEnum] ) extends HasSource { override val source: Source.Table = Source.Table(dbTable.name) @@ -47,22 +49,32 @@ case class ComputedTable( case NonEmptyList(colName, Nil) => val dbCol = dbColsByName(colName) val pointsTo = deps.getOrElse(dbCol.name, Nil) + lazy val underlying = scalaTypeMapper.col(dbTable.name, dbCol, None) - findTypeFromFk(options.logger, source, dbCol.name, pointsTo, eval.asMaybe)(_ => None) match { - case Some(tpe) => + val fromFk: Option[IdComputed.UnaryInherited] = + findTypeFromFk(options.logger, source, dbCol.name, pointsTo, eval.asMaybe)(_ => None).map { tpe => val col = ComputedColumn(pointsTo = pointsTo, name = naming.field(dbCol.name), tpe = tpe, dbCol = dbCol) - Some(IdComputed.UnaryInherited(col, tpe)) - case None => - val underlying = scalaTypeMapper.col(dbTable.name, dbCol, None) - val col = ComputedColumn(pointsTo = pointsTo, name = naming.field(dbCol.name), tpe = underlying, dbCol = dbCol) - if (sc.Type.containsUserDefined(underlying)) - Some(IdComputed.UnaryUserSpecified(col, underlying)) - else if (!options.enablePrimaryKeyType.include(dbTable.name)) - Some(IdComputed.UnaryNoIdType(col, underlying)) - else { - val tpe = sc.Type.Qualified(naming.idName(source, List(col.dbCol))) - Some(IdComputed.UnaryNormal(col, tpe)) - } + IdComputed.UnaryInherited(col, tpe) + } + + val fromOpenEnum: Option[IdComputed.UnaryOpenEnum] = + openEnumsByTable.get(dbTable.name).map { values => + val tpe = sc.Type.Qualified(naming.idName(source, List(dbCol))) + val col = ComputedColumn(pointsTo = pointsTo, name = naming.field(dbCol.name), tpe = tpe, dbCol = dbCol) + IdComputed.UnaryOpenEnum(col, tpe, underlying, values) + } + + fromFk.orElse(fromOpenEnum).orElse { + val underlying = scalaTypeMapper.col(dbTable.name, dbCol, None) + val col = ComputedColumn(pointsTo = pointsTo, name = naming.field(dbCol.name), tpe = underlying, dbCol = dbCol) + if (sc.Type.containsUserDefined(underlying)) + Some(IdComputed.UnaryUserSpecified(col, underlying)) + else if (!options.enablePrimaryKeyType.include(dbTable.name)) + Some(IdComputed.UnaryNoIdType(col, underlying)) + else { + val tpe = sc.Type.Qualified(naming.idName(source, List(col.dbCol))) + Some(IdComputed.UnaryNormal(col, tpe)) + } } case colNames => diff --git a/typo/src/scala/typo/internal/ComputedTestInserts.scala b/typo/src/scala/typo/internal/ComputedTestInserts.scala index 2372a3125c..048e734fb8 100644 --- a/typo/src/scala/typo/internal/ComputedTestInserts.scala +++ b/typo/src/scala/typo/internal/ComputedTestInserts.scala @@ -4,6 +4,7 @@ package internal import typo.db.Type import typo.internal.codegen.* import typo.internal.compat.* +import typo.internal.metadb.OpenEnum case class ComputedTestInserts(tpe: sc.Type.Qualified, methods: List[ComputedTestInserts.InsertMethod], maybeDomainMethods: Option[ComputedTestInserts.GenerateDomainMethods]) @@ -25,6 +26,9 @@ object ComputedTestInserts { val enumsByName: Map[sc.Type, ComputedStringEnum] = enums.iterator.map(x => x.tpe -> x).toMap + val openEnumsByType: Map[sc.Type, OpenEnum] = + tables.flatMap { table => table.maybeId.collect { case x: IdComputed.UnaryOpenEnum => (x.tpe, x.openEnum) } }.toMap + val maybeDomainMethods: Option[GenerateDomainMethods] = GenerateDomainMethod .of(domains, tables) @@ -46,8 +50,12 @@ object ComputedTestInserts { case x: IdComputed.UnaryNormal => go(x.underlying, x.col.dbCol.tpe, None).map(default => code"${x.tpe}($default)") case x: IdComputed.UnaryInherited => go(x.underlying, x.col.dbCol.tpe, None).map(default => code"${x.tpe}($default)") case x: IdComputed.UnaryNoIdType => go(x.underlying, x.col.dbCol.tpe, None) + case x: IdComputed.UnaryOpenEnum => go(x.underlying, x.col.dbCol.tpe, None).map(default => code"${x.tpe}($default)") case _: IdComputed.UnaryUserSpecified => None } + case tpe if openEnumsByType.contains(tpe) => + val openEnum = openEnumsByType(tpe) + Some(code"${tpe}.All($random.nextInt(${openEnum.values.length}))") case TypesJava.String => val max: Int = Option(dbType) @@ -129,7 +137,8 @@ object ComputedTestInserts { def defaultedParametersFor(cols: List[ComputedColumn]): List[sc.Param] = { val params = cols.map { col => val isMeaningful = hasConstraints(col.dbName) || appearsInFkButNotPk(col.dbName) - val default = if (isMeaningful && !col.dbCol.isDefaulted) { + val isOpenEnum = openEnumsByType.contains(col.tpe) + val default = if (isMeaningful && !col.dbCol.isDefaulted && !isOpenEnum) { if (col.dbCol.nullability == Nullability.NoNulls) None else Some(TypesScala.None.code) } else defaultFor(table, col.tpe, col.dbCol.tpe) sc.Param(col.name, col.tpe, default) diff --git a/typo/src/scala/typo/internal/IdComputed.scala b/typo/src/scala/typo/internal/IdComputed.scala index fa9744a01a..38f5ab09ef 100644 --- a/typo/src/scala/typo/internal/IdComputed.scala +++ b/typo/src/scala/typo/internal/IdComputed.scala @@ -2,6 +2,7 @@ package typo package internal import typo.internal.compat.* +import typo.internal.metadb.OpenEnum sealed trait IdComputed { def paramName: sc.Ident @@ -36,6 +37,13 @@ object IdComputed { def underlying: sc.Type = col.tpe } + case class UnaryOpenEnum( + col: ComputedColumn, + tpe: sc.Type.Qualified, + underlying: sc.Type, + openEnum: OpenEnum + ) extends Unary + // if user supplied a type override for an id column case class UnaryUserSpecified(col: ComputedColumn, tpe: sc.Type) extends Unary { override def paramName: sc.Ident = col.name diff --git a/typo/src/scala/typo/internal/TypeMapperDb.scala b/typo/src/scala/typo/internal/TypeMapperDb.scala index 0fde7f951c..58e5adb6a1 100644 --- a/typo/src/scala/typo/internal/TypeMapperDb.scala +++ b/typo/src/scala/typo/internal/TypeMapperDb.scala @@ -78,7 +78,7 @@ case class TypeMapperDb(enums: List[db.StringEnum], domains: List[db.Domain]) { case typeName => enumsByName .get(typeName) - .map(`enum` => db.Type.EnumRef(`enum`.name)) + .map(db.Type.EnumRef.apply) .orElse(domainsByName.get(typeName).map(domain => db.Type.DomainRef(domain.name, domain.originalType, domain.tpe))) .getOrElse { logWarning() diff --git a/typo/src/scala/typo/internal/TypeMapperScala.scala b/typo/src/scala/typo/internal/TypeMapperScala.scala index 41c2082c09..706cbf3577 100644 --- a/typo/src/scala/typo/internal/TypeMapperScala.scala +++ b/typo/src/scala/typo/internal/TypeMapperScala.scala @@ -101,7 +101,7 @@ case class TypeMapperScala( case db.Type.regrole => customTypes.TypoRegrole.typoType case db.Type.regtype => customTypes.TypoRegtype.typoType case db.Type.xid => customTypes.TypoXid.typoType - case db.Type.EnumRef(name) => sc.Type.Qualified(naming.enumName(name)) + case db.Type.EnumRef(enm) => sc.Type.Qualified(naming.enumName(enm.name)) case db.Type.Text => TypesJava.String case db.Type.Time => customTypes.TypoLocalTime.typoType case db.Type.TimeTz => customTypes.TypoOffsetTime.typoType diff --git a/typo/src/scala/typo/internal/codegen/DbLib.scala b/typo/src/scala/typo/internal/codegen/DbLib.scala index 4433915ddd..2a5ebb21a1 100644 --- a/typo/src/scala/typo/internal/codegen/DbLib.scala +++ b/typo/src/scala/typo/internal/codegen/DbLib.scala @@ -8,7 +8,7 @@ trait DbLib { def repoImpl(repoMethod: RepoMethod): sc.Code def mockRepoImpl(id: IdComputed, repoMethod: RepoMethod, maybeToRow: Option[sc.Param]): sc.Code def testInsertMethod(x: ComputedTestInserts.InsertMethod): sc.Value - def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, enm: db.StringEnum): List[sc.ClassMember] + def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, sqlType: String, openEnum: Boolean): List[sc.ClassMember] def wrapperTypeInstances(wrapperType: sc.Type.Qualified, underlying: sc.Type, overrideDbType: Option[String]): List[sc.ClassMember] def missingInstances: List[sc.ClassMember] def rowInstances(tpe: sc.Type, cols: NonEmptyList[ComputedColumn], rowType: DbLib.RowType): List[sc.ClassMember] diff --git a/typo/src/scala/typo/internal/codegen/DbLibAnorm.scala b/typo/src/scala/typo/internal/codegen/DbLibAnorm.scala index 5c84166b17..cdce32176d 100644 --- a/typo/src/scala/typo/internal/codegen/DbLibAnorm.scala +++ b/typo/src/scala/typo/internal/codegen/DbLibAnorm.scala @@ -719,8 +719,8 @@ class DbLibAnorm(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDefa override val defaultedInstance: List[sc.Given] = textSupport.map(_.defaultedInstance).toList - override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, enm: db.StringEnum): List[sc.Given] = { - val sqlTypeLit = sc.StrLit(enm.name.value) + override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, sqlType: String, openEnum: Boolean): List[sc.Given] = { + val sqlTypeLit = sc.StrLit(sqlType) val arrayWrapper = sc.Type.ArrayOf(wrapperType) List( Some( @@ -729,7 +729,9 @@ class DbLibAnorm(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDefa name = arrayColumnName, implicitParams = Nil, tpe = Column.of(arrayWrapper), - body = code"""$Column.columnToArray($columnName, implicitly)""" + body = + if (openEnum) code"${lookupColumnFor(sc.Type.ArrayOf(underlying))}.map(_.map($wrapperType.apply))" + else code"${lookupColumnFor(sc.Type.ArrayOf(underlying))}.map(_.map($wrapperType.force))" ) ), Some( @@ -738,7 +740,9 @@ class DbLibAnorm(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDefa name = columnName, implicitParams = Nil, tpe = Column.of(wrapperType), - body = code"""${lookupColumnFor(underlying)}.mapResult(str => $wrapperType(str).left.map($SqlMappingError.apply))""" + body = + if (openEnum) code"${lookupColumnFor(underlying)}.map($wrapperType.apply)" + else code"${lookupColumnFor(underlying)}.mapResult(str => $wrapperType(str).left.map($SqlMappingError.apply))" ) ), Some( @@ -756,7 +760,9 @@ class DbLibAnorm(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDefa name = arrayToStatementName, implicitParams = Nil, tpe = ToStatement.of(arrayWrapper), - body = code"$ToStatement[$arrayWrapper]((ps, i, arr) => ps.setArray(i, ps.getConnection.createArrayOf($sqlTypeLit, arr.map[AnyRef](_.value))))" + body = + if (openEnum) code"${lookupToStatementFor(sc.Type.ArrayOf(underlying))}.contramap(_.map(_.value))" + else code"$ToStatement[$arrayWrapper]((ps, i, arr) => ps.setArray(i, ps.getConnection.createArrayOf($sqlTypeLit, arr.map[AnyRef](_.value))))" ) ), Some( diff --git a/typo/src/scala/typo/internal/codegen/DbLibDoobie.scala b/typo/src/scala/typo/internal/codegen/DbLibDoobie.scala index d63ecc4803..039567118e 100644 --- a/typo/src/scala/typo/internal/codegen/DbLibDoobie.scala +++ b/typo/src/scala/typo/internal/codegen/DbLibDoobie.scala @@ -593,9 +593,9 @@ class DbLibDoobie(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDef override val defaultedInstance: List[sc.Given] = textSupport.map(_.defaultedInstance).toList - override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, enm: db.StringEnum): List[sc.Given] = { - val sqlTypeLit = sc.StrLit(enm.name.value) - val sqlArrayTypeLit = sc.StrLit(enm.name.value + "[]") + override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, sqlType: String, openEnum: Boolean): List[sc.Given] = { + val sqlTypeLit = sc.StrLit(sqlType) + val sqlArrayTypeLit = sc.StrLit(sqlType + "[]") List( Some( sc.Given( @@ -603,7 +603,9 @@ class DbLibDoobie(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDef name = putName, implicitParams = Nil, tpe = Put.of(wrapperType), - body = code"$Put.Advanced.one[$wrapperType]($JdbcType.Other, $NonEmptyList.one($sqlTypeLit), (ps, i, a) => ps.setString(i, a.value), (rs, i, a) => rs.updateString(i, a.value))" + body = + if (openEnum) code"${lookupPutFor(underlying)}.contramap(_.value)" + else code"$Put.Advanced.one[$wrapperType]($JdbcType.Other, $NonEmptyList.one($sqlTypeLit), (ps, i, a) => ps.setString(i, a.value), (rs, i, a) => rs.updateString(i, a.value))" ) ), Some( @@ -612,7 +614,9 @@ class DbLibDoobie(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDef name = arrayPutName, implicitParams = Nil, tpe = Put.of(sc.Type.ArrayOf(wrapperType)), - body = code"$Put.Advanced.array[${TypesScala.AnyRef}]($NonEmptyList.one($sqlArrayTypeLit), $sqlTypeLit).contramap(_.map(_.value))" + body = + if (openEnum) code"${lookupPutFor(sc.Type.ArrayOf(underlying))}.contramap(_.map(_.value))" + else code"$Put.Advanced.array[${TypesScala.AnyRef}]($NonEmptyList.one($sqlArrayTypeLit), $sqlTypeLit).contramap(_.map(_.value))" ) ), Some( @@ -621,7 +625,9 @@ class DbLibDoobie(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDef name = getName, implicitParams = Nil, tpe = Get.of(wrapperType), - body = code"""${lookupGetFor(underlying)}.temap($wrapperType.apply)""" + body = + if (openEnum) code"""${lookupGetFor(underlying)}.map($wrapperType.apply)""" + else code"""${lookupGetFor(underlying)}.temap($wrapperType.apply)""" ) ), Some( @@ -630,7 +636,11 @@ class DbLibDoobie(pkg: sc.QIdent, inlineImplicits: Boolean, default: ComputedDef name = arrayGetName, implicitParams = Nil, tpe = Get.of(sc.Type.ArrayOf(wrapperType)), - body = code"${lookupGetFor(sc.Type.ArrayOf(underlying))}.map(_.map(force))" + body = { + val get = lookupGetFor(sc.Type.ArrayOf(underlying)) + if (openEnum) code"""$get.map(_.map($wrapperType.apply))""" + else code"$get.map(_.map(force))" + } ) ), Some( diff --git a/typo/src/scala/typo/internal/codegen/DbLibZioJdbc.scala b/typo/src/scala/typo/internal/codegen/DbLibZioJdbc.scala index bf5f1758c3..bf46931c0b 100644 --- a/typo/src/scala/typo/internal/codegen/DbLibZioJdbc.scala +++ b/typo/src/scala/typo/internal/codegen/DbLibZioJdbc.scala @@ -662,15 +662,17 @@ class DbLibZioJdbc(pkg: sc.QIdent, inlineImplicits: Boolean, dslEnabled: Boolean override val defaultedInstance: List[sc.Given] = textSupport.map(_.defaultedInstance).toList - override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, enm: db.StringEnum): List[sc.ClassMember] = { - val sqlTypeLit = sc.StrLit(enm.name.value) + override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, sqlType: String, openEnum: Boolean): List[sc.ClassMember] = { + val sqlTypeLit = sc.StrLit(sqlType) val arrayWrapper = sc.Type.ArrayOf(wrapperType) val arraySetter = sc.Given( tparams = Nil, name = arraySetterName, implicitParams = Nil, tpe = Setter.of(arrayWrapper), - body = code"""|$Setter.forSqlType[$arrayWrapper]( + body = + if (openEnum) code"${lookupSetter(sc.Type.ArrayOf(underlying))}.contramap(_.map(_.value))" + else code"""|$Setter.forSqlType[$arrayWrapper]( | (ps, i, v) => ps.setArray(i, ps.getConnection.createArrayOf($sqlTypeLit, v.map(x => x.value))), | java.sql.Types.ARRAY | )""".stripMargin @@ -680,7 +682,10 @@ class DbLibZioJdbc(pkg: sc.QIdent, inlineImplicits: Boolean, dslEnabled: Boolean name = arrayJdbcDecoderName, implicitParams = Nil, tpe = JdbcDecoder.of(arrayWrapper), - body = code"""${lookupJdbcDecoder(sc.Type.ArrayOf(underlying))}.map(a => if (a == null) null else a.map(force))""" + body = + if (openEnum) + code"""${lookupJdbcDecoder(sc.Type.ArrayOf(underlying))}.map(a => if (a == null) null else a.map(apply))""" + else code"""${lookupJdbcDecoder(sc.Type.ArrayOf(underlying))}.map(a => if (a == null) null else a.map(force))""" ) val arrayJdbcEncoder = sc.Given( tparams = Nil, @@ -699,21 +704,22 @@ class DbLibZioJdbc(pkg: sc.QIdent, inlineImplicits: Boolean, dslEnabled: Boolean ) val jdbcDecoder = { val body = - code"""|${lookupJdbcDecoder(underlying)}.flatMap { s => - | new ${JdbcDecoder.of(wrapperType)} { - | override def unsafeDecode(columIndex: ${TypesScala.Int}, rs: ${TypesJava.ResultSet}): (${TypesScala.Int}, $wrapperType) = { - | def error(msg: ${TypesJava.String}): $JdbcDecoderError = - | $JdbcDecoderError( - | message = s"Error decoding $wrapperType from ResultSet", - | cause = new RuntimeException(msg), - | metadata = rs.getMetaData, - | row = rs.getRow - | ) - | - | $wrapperType.apply(s).fold(e => throw error(e), (columIndex, _)) - | } - | } - |}""".stripMargin + if (openEnum) code"${lookupJdbcDecoder(underlying)}.map($wrapperType.apply)" + else code"""|${lookupJdbcDecoder(underlying)}.flatMap { s => + | new ${JdbcDecoder.of(wrapperType)} { + | override def unsafeDecode(columIndex: ${TypesScala.Int}, rs: ${TypesJava.ResultSet}): (${TypesScala.Int}, $wrapperType) = { + | def error(msg: ${TypesJava.String}): $JdbcDecoderError = + | $JdbcDecoderError( + | message = s"Error decoding $wrapperType from ResultSet", + | cause = new RuntimeException(msg), + | metadata = rs.getMetaData, + | row = rs.getRow + | ) + | + | $wrapperType.apply(s).fold(e => throw error(e), (columIndex, _)) + | } + | } + |}""".stripMargin sc.Given(tparams = Nil, name = jdbcDecoderName, implicitParams = Nil, tpe = JdbcDecoder.of(wrapperType), body = body) } diff --git a/typo/src/scala/typo/internal/codegen/FileStringEnum.scala b/typo/src/scala/typo/internal/codegen/FileStringEnum.scala index d74072809d..b2ff75198b 100644 --- a/typo/src/scala/typo/internal/codegen/FileStringEnum.scala +++ b/typo/src/scala/typo/internal/codegen/FileStringEnum.scala @@ -5,7 +5,7 @@ package codegen object FileStringEnum { def apply(options: InternalOptions, enm: ComputedStringEnum, genOrdering: GenOrdering): sc.File = { - val comments = scaladoc(s"Enum `${enm.name.value}`")(enm.members.map { case (_, v) => " - " + v }) + val comments = scaladoc(s"Enum `${enm.dbEnum.name.value}`")(enm.members.toList.map { case (_, v) => " - " + v }) val members = enm.members.map { case (name, value) => name -> code"case object $name extends ${enm.tpe.name}(${sc.StrLit(value)})" @@ -13,8 +13,8 @@ object FileStringEnum { val str = sc.Ident("str") val instances = List( - options.dbLib.toList.flatMap(_.stringEnumInstances(enm.tpe, TypesJava.String, enm.dbEnum)), - options.jsonLibs.flatMap(_.stringEnumInstances(enm.tpe, TypesJava.String)), + options.dbLib.toList.flatMap(_.stringEnumInstances(enm.tpe, TypesJava.String, enm.dbEnum.name.value, openEnum = false)), + options.jsonLibs.flatMap(_.stringEnumInstances(enm.tpe, TypesJava.String, openEnum = false)), List( genOrdering.ordering(enm.tpe, NonEmptyList(sc.Param(sc.Ident("value"), TypesJava.String, None))) ) diff --git a/typo/src/scala/typo/internal/codegen/FilesTable.scala b/typo/src/scala/typo/internal/codegen/FilesTable.scala index 5c8b8d11b5..cbf7c2ed5d 100644 --- a/typo/src/scala/typo/internal/codegen/FilesTable.scala +++ b/typo/src/scala/typo/internal/codegen/FilesTable.scala @@ -4,6 +4,7 @@ package codegen import play.api.libs.json.Json import typo.internal.codegen.DbLib.RowType +import typo.internal.metadb.OpenEnum case class FilesTable(table: ComputedTable, fkAnalysis: FkAnalysis, options: InternalOptions, genOrdering: GenOrdering, domainsByName: Map[db.RelationName, ComputedDomain]) { val relation = FilesRelation(table.naming, table.names, Some(table.cols), Some(fkAnalysis), options, table.dbTable.foreignKeys) @@ -147,6 +148,72 @@ case class FilesTable(table: ComputedTable, fkAnalysis: FkAnalysis, options: Int scope = Scope.Main ) ) + case x: IdComputed.UnaryOpenEnum => + val comments = scaladoc(s"Type for the primary key of table `${table.dbTable.name.value}`. It has some known values: ")(x.openEnum.values.toList.map { v => " - " + v }) + val Underlying: sc.Type.Qualified = + x.openEnum match { + case OpenEnum.Text(_) => + TypesJava.String + case OpenEnum.TextDomain(domainRef, _) => + sc.Type.Qualified(options.naming.domainName(domainRef.name)) + } + val underlying = sc.Ident("underlying") + + val members = x.openEnum.values.map { value => + val name = options.naming.enumValue(value) + x.openEnum match { + case OpenEnum.Text(_) => + (name, code"case object $name extends ${x.tpe.name}(${sc.StrLit(value)})") + case OpenEnum.TextDomain(_, _) => + (name, code"case object $name extends ${x.tpe.name}(${Underlying}(${sc.StrLit(value)}))") + } + } + + // shortcut for id files wrapping a domain + val maybeFromString: Option[sc.Value] = + x.openEnum match { + case OpenEnum.Text(_) => None + case OpenEnum.TextDomain(db.Type.DomainRef(name, _, _), _) => + domainsByName.get(name).map { domain => + val name = domain.underlying.constraintDefinition match { + case Some(_) => domain.tpe.name.map(Naming.camelCase) + case None => sc.Ident("apply") + } + val value = sc.Ident("value") + sc.Value(Nil, name, List(sc.Param(value, domain.underlyingType, None)), Nil, x.tpe, code"${x.tpe}(${domain.tpe}($value))") + } + } + + val sqlType = x.openEnum match { + case OpenEnum.Text(_) => "text" + case OpenEnum.TextDomain(domainRef, _) => domainRef.name.quotedValue + } + + val instances = List( + options.dbLib.toList.flatMap(_.stringEnumInstances(x.tpe, x.underlying, sqlType, openEnum = true)), + options.jsonLibs.flatMap(_.stringEnumInstances(x.tpe, x.underlying, openEnum = true)), + List( + genOrdering.ordering(x.tpe, NonEmptyList(sc.Param(sc.Ident("value"), TypesJava.String, None))) + ) + ).flatten + + val obj = genObject.withBody(x.tpe.value, instances)( + code"""|def apply($underlying: $Underlying): ${x.tpe} = + | ByName.getOrElse($underlying, Unknown($underlying)) + |${(maybeFromString.map(_.code).toList ++ members.toList.map { case (_, definition) => definition }).mkCode("\n")} + |case class Unknown(override val value: $Underlying) extends ${x.tpe}(value) + |val All: ${TypesScala.List.of(x.tpe)} = ${TypesScala.List}(${members.map { case (ident, _) => ident.code }.mkCode(", ")}) + |val ByName: ${TypesScala.Map.of(Underlying, x.tpe)} = All.map(x => (x.value, x)).toMap + """.stripMargin + ) + val body = + code"""|$comments + |sealed abstract class ${x.tpe.name}(val value: ${Underlying}) + | + |$obj + |""".stripMargin + + Some(sc.File(x.tpe, body, secondaryTypes = Nil, scope = Scope.Main)) case _: IdComputed.UnaryUserSpecified | _: IdComputed.UnaryNoIdType | _: IdComputed.UnaryInherited => None diff --git a/typo/src/scala/typo/internal/codegen/JsonLib.scala b/typo/src/scala/typo/internal/codegen/JsonLib.scala index e2cbc2c309..9b2aa4d2d3 100644 --- a/typo/src/scala/typo/internal/codegen/JsonLib.scala +++ b/typo/src/scala/typo/internal/codegen/JsonLib.scala @@ -4,7 +4,7 @@ package codegen trait JsonLib { def defaultedInstance(default: ComputedDefault): List[sc.Given] - def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type): List[sc.Given] + def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, openEnum: Boolean): List[sc.Given] def wrapperTypeInstances(wrapperType: sc.Type.Qualified, fieldName: sc.Ident, underlying: sc.Type): List[sc.Given] def productInstances(tpe: sc.Type, fields: NonEmptyList[JsonLib.Field]): List[sc.Given] def missingInstances: List[sc.ClassMember] diff --git a/typo/src/scala/typo/internal/codegen/JsonLibCirce.scala b/typo/src/scala/typo/internal/codegen/JsonLibCirce.scala index 8bbc186050..7c1e539c1f 100644 --- a/typo/src/scala/typo/internal/codegen/JsonLibCirce.scala +++ b/typo/src/scala/typo/internal/codegen/JsonLibCirce.scala @@ -114,14 +114,16 @@ case class JsonLibCirce(pkg: sc.QIdent, default: ComputedDefault, inlineImplicit ) } - override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type): List[sc.Given] = + override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, openEnum: Boolean): List[sc.Given] = List( sc.Given( tparams = Nil, name = decoderName, implicitParams = Nil, tpe = Decoder.of(wrapperType), - body = code"""${lookupDecoderFor(underlying)}.emap($wrapperType.apply)""" + body = + if (openEnum: Boolean) code"""${lookupDecoderFor(underlying)}.map($wrapperType.apply)""" + else code"""${lookupDecoderFor(underlying)}.emap($wrapperType.apply)""" ), sc.Given( tparams = Nil, diff --git a/typo/src/scala/typo/internal/codegen/JsonLibPlay.scala b/typo/src/scala/typo/internal/codegen/JsonLibPlay.scala index e49644b681..6f95e0926f 100644 --- a/typo/src/scala/typo/internal/codegen/JsonLibPlay.scala +++ b/typo/src/scala/typo/internal/codegen/JsonLibPlay.scala @@ -128,14 +128,19 @@ case class JsonLibPlay(pkg: sc.QIdent, default: ComputedDefault, inlineImplicits List(reader, readerOpt, writer) } - override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type): List[sc.Given] = + override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, openEnum: Boolean): List[sc.Given] = List( sc.Given( tparams = Nil, name = readsName, implicitParams = Nil, tpe = Reads.of(wrapperType), - body = code"""${Reads.of(wrapperType)}{(value: $JsValue) => value.validate(${lookupReadsFor(underlying)}).flatMap(str => $wrapperType(str).fold($JsError.apply, $JsSuccess(_)))}""" + body = { + if (openEnum) + code"${Reads.of(wrapperType)}{(value: $JsValue) => value.validate(${lookupReadsFor(underlying)}).map($wrapperType.apply)}" + else + code"${Reads.of(wrapperType)}{(value: $JsValue) => value.validate(${lookupReadsFor(underlying)}).flatMap(str => $wrapperType(str).fold($JsError.apply, $JsSuccess(_)))}" + } ), sc.Given( tparams = Nil, diff --git a/typo/src/scala/typo/internal/codegen/JsonLibZioJson.scala b/typo/src/scala/typo/internal/codegen/JsonLibZioJson.scala index 5e032438b3..54a1a19c0d 100644 --- a/typo/src/scala/typo/internal/codegen/JsonLibZioJson.scala +++ b/typo/src/scala/typo/internal/codegen/JsonLibZioJson.scala @@ -108,14 +108,16 @@ final case class JsonLibZioJson(pkg: sc.QIdent, default: ComputedDefault, inline ) } - override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type): List[sc.Given] = + override def stringEnumInstances(wrapperType: sc.Type, underlying: sc.Type, openEnum: Boolean): List[sc.Given] = List( sc.Given( tparams = Nil, name = decoderName, implicitParams = Nil, tpe = JsonDecoder.of(wrapperType), - body = code"""${lookupDecoderFor(underlying)}.mapOrFail($wrapperType.apply)""" + body = + if (openEnum) code"""${lookupDecoderFor(underlying)}.map($wrapperType.apply)""" + else code"""${lookupDecoderFor(underlying)}.mapOrFail($wrapperType.apply)""" ), sc.Given( tparams = Nil, diff --git a/typo/src/scala/typo/internal/codegen/SqlCast.scala b/typo/src/scala/typo/internal/codegen/SqlCast.scala index 86050fe250..9fd912ad20 100644 --- a/typo/src/scala/typo/internal/codegen/SqlCast.scala +++ b/typo/src/scala/typo/internal/codegen/SqlCast.scala @@ -24,7 +24,7 @@ object SqlCast { def toPg(dbType: db.Type, udtName: Option[String]): Option[SqlCast] = dbType match { case db.Type.Unknown(sqlType) => Some(SqlCast(sqlType)) - case db.Type.EnumRef(name) => Some(SqlCast(name.value)) + case db.Type.EnumRef(enm) => Some(SqlCast(enm.name.value)) case db.Type.Boolean | db.Type.Text | db.Type.VarChar(_) => None case _ => udtName.map { diff --git a/typo/src/scala/typo/internal/generate.scala b/typo/src/scala/typo/internal/generate.scala index 24648dbc89..badfd7ae15 100644 --- a/typo/src/scala/typo/internal/generate.scala +++ b/typo/src/scala/typo/internal/generate.scala @@ -2,6 +2,7 @@ package typo package internal import typo.internal.codegen.* +import typo.internal.metadb.OpenEnum import typo.internal.sqlfiles.SqlFile import scala.collection.immutable @@ -11,7 +12,12 @@ object generate { private type Files = Map[sc.Type.Qualified, sc.File] // use this constructor if you need to run `typo` multiple times with different options but same database/scripts - def apply(publicOptions: Options, metaDb0: MetaDb, graph: ProjectGraph[Selector, List[SqlFile]]): List[Generated] = { + def apply( + publicOptions: Options, + metaDb0: MetaDb, + graph: ProjectGraph[Selector, List[SqlFile]], + openEnumsByTable: Map[db.RelationName, OpenEnum] + ): List[Generated] = { Banner.maybePrint(publicOptions) val metaDb = publicOptions.rewriteDatabase(metaDb0) val pkg = sc.Type.Qualified(publicOptions.pkg).value @@ -21,7 +27,6 @@ object generate { ComputedDefault(publicOptions.naming(customTypesPackage)) val naming = publicOptions.naming(pkg) - val options = InternalOptions( dbLib = publicOptions.dbLib.map { case DbLibName.Anorm => new DbLibAnorm(pkg, publicOptions.inlineImplicits, default, publicOptions.enableStreamingInserts) @@ -63,7 +68,7 @@ object generate { val computedLazyRelations: SortedMap[db.RelationName, Lazy[HasSource]] = rewriteDependentData(metaDb.relations).apply[HasSource] { case (_, dbTable: db.Table, eval) => - ComputedTable(options, default, dbTable, naming, scalaTypeMapper, eval) + ComputedTable(options, default, dbTable, naming, scalaTypeMapper, eval, openEnumsByTable) case (_, dbView: db.View, eval) => ComputedView(options.logger, dbView, naming, metaDb.typeMapperDb, scalaTypeMapper, eval, options.enableFieldValue.include(dbView.name), options.enableDsl) } diff --git a/typo/src/scala/typo/internal/metadb/Enums.scala b/typo/src/scala/typo/internal/metadb/Enums.scala index b1083c1f37..89de815b85 100644 --- a/typo/src/scala/typo/internal/metadb/Enums.scala +++ b/typo/src/scala/typo/internal/metadb/Enums.scala @@ -8,8 +8,10 @@ object Enums { def apply(pgEnums: List[EnumsSqlRow]): List[db.StringEnum] = { pgEnums .groupBy(row => db.RelationName(row.enumSchema, row.enumName)) - .map { case (relName, values: Seq[EnumsSqlRow]) => - db.StringEnum(relName, values.sortBy(_.enumSortOrder).map(_.enumValue)) + .flatMap { case (relName, values) => + NonEmptyList + .fromList(values.sortBy(_.enumSortOrder)) + .map(values => db.StringEnum(relName, values.map(_.enumValue))) } .toList } diff --git a/typo/src/scala/typo/internal/metadb/OpenEnum.scala b/typo/src/scala/typo/internal/metadb/OpenEnum.scala new file mode 100644 index 0000000000..9eecd13938 --- /dev/null +++ b/typo/src/scala/typo/internal/metadb/OpenEnum.scala @@ -0,0 +1,58 @@ +package typo +package internal +package metadb + +import anorm.* + +import scala.concurrent.{ExecutionContext, Future} + +sealed trait OpenEnum { + val values: NonEmptyList[String] +} + +object OpenEnum { + case class Text(values: NonEmptyList[String]) extends OpenEnum + case class TextDomain(domainRef: db.Type.DomainRef, values: NonEmptyList[String]) extends OpenEnum + + def find( + ds: TypoDataSource, + logger: TypoLogger, + // optimization to not necessarily evaluate all relations + viewSelector: Selector, + openEnumSelector: Selector, + metaDb: MetaDb + )(implicit ec: ExecutionContext): Future[Map[db.RelationName, OpenEnum]] = + Future + .sequence { + def fetch(dbTable: db.Table, unaryPkCol: db.Col): Future[Option[(db.RelationName, NonEmptyList[String])]] = + ds.run { implicit c => + logger.info(s"Fetching enum values for ${dbTable.name.value}") + val values = SQL"""select "#${unaryPkCol.name.value}"::text from #${dbTable.name.quotedValue}""".as(SqlParser.str(1).*) + NonEmptyList.fromList(values.sorted) match { + case Some(nonEmptyValues) => + Some((dbTable.name, nonEmptyValues)) + case None => + logger.warn(s"Table ${dbTable.name.value} has no values for enum column ${unaryPkCol.name.value}") + None + } + } + + for { + tuple <- metaDb.relations + (name, lazyRelation) = tuple + if viewSelector.include(name) && openEnumSelector.include(name) + dbTable <- lazyRelation.get.collect { case dbTable: db.Table => dbTable } + unaryPkCol <- dbTable.primaryKey.collect { case db.PrimaryKey(NonEmptyList(head, Nil), _) => dbTable.cols.find(_.name == head) }.flatten + } yield { + unaryPkCol.tpe match { + case db.Type.Text | db.Type.VarChar(_) => + fetch(dbTable, unaryPkCol).map(_.map { case (name, values) => (name, OpenEnum.Text(values)) }) + case domainRef @ db.Type.DomainRef(_, _, db.Type.Text | db.Type.VarChar(_)) => + fetch(dbTable, unaryPkCol).map(_.map { case (name, values) => (name, OpenEnum.TextDomain(domainRef, values)) }) + case _ => Future.successful(None) + } + } + } + .map(_.flatten.toMap) + +}