Skip to content

Commit

Permalink
Support GENERATED ALWAYS (part 2) (#134)
Browse files Browse the repository at this point in the history
* Support `GENERATED ALWAYS` without `IDENTITY`

* `Unsaved` rows for tables with generated always columns
  • Loading branch information
oyvindberg authored Aug 16, 2024
1 parent 35c1757 commit dc03273
Show file tree
Hide file tree
Showing 39 changed files with 1,464 additions and 74 deletions.
33 changes: 29 additions & 4 deletions init/data/test-tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,40 @@ 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
(
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 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
);

create table "table-with-generated-columns"
(
name text primary key,
"name-type-always" text NOT NULL GENERATED ALWAYS AS
(CASE
WHEN name IS NOT NULL THEN 'no-name'
WHEN name = 'a' THEN 'a-name'
ELSE 'some-name'
END) STORED
)
8 changes: 4 additions & 4 deletions typo-scripts/src/scala/scripts/GenHardcodedFiles.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
udtName = Some("varchar"),
nullability = Nullability.NoNulls,
columnDefault = None,
identity = None,
maybeGenerated = None,
comment = None,
constraints = Nil,
jsonDescription = DebugJson.Empty
Expand All @@ -41,7 +41,7 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
udtName = Some("varchar"),
nullability = Nullability.NoNulls,
columnDefault = Some("some-value"),
identity = None,
maybeGenerated = None,
comment = None,
constraints = Nil,
jsonDescription = DebugJson.Empty
Expand All @@ -53,7 +53,7 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
udtName = Some("myschema.sector"),
nullability = Nullability.NoNulls,
columnDefault = Some("PUBLIC"),
identity = Some(db.Identity("ALWAYS", None, None, None, None)),
maybeGenerated = Some(db.Generated.Identity("ALWAYS", None, None, None, None)),
comment = None,
constraints = Nil,
jsonDescription = DebugJson.Empty
Expand All @@ -64,7 +64,7 @@ object GenHardcodedFiles extends BleepCodegenScript("GenHardcodedFiles") {
udtName = Some("myschema.number"),
nullability = Nullability.NoNulls,
columnDefault = Some("one"),
identity = None,
maybeGenerated = None,
comment = None,
constraints = Nil,
jsonDescription = DebugJson.Empty
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* File has been automatically generated by `typo`.
*
* IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN.
*/
package adventureworks
package public
package table_with_generated_columns

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

trait TableWithGeneratedColumnsFields {
def name: IdField[TableWithGeneratedColumnsId, TableWithGeneratedColumnsRow]
def nameTypeAlways: Field[String, TableWithGeneratedColumnsRow]
}

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

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

override lazy val fields: TableWithGeneratedColumnsFields = new TableWithGeneratedColumnsFields {
override def name = IdField[TableWithGeneratedColumnsId, TableWithGeneratedColumnsRow](_path, "name", None, None, x => x.name, (row, value) => row.copy(name = value))
override def nameTypeAlways = Field[String, TableWithGeneratedColumnsRow](_path, "name-type-always", None, None, x => x.nameTypeAlways, (row, value) => row.copy(nameTypeAlways = value))
}

override lazy val columns: List[FieldLikeNoHkt[?, TableWithGeneratedColumnsRow]] =
List[FieldLikeNoHkt[?, TableWithGeneratedColumnsRow]](fields.name, fields.nameTypeAlways)

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

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

import anorm.Column
import anorm.ParameterMetaData
import anorm.ToStatement
import play.api.libs.json.Reads
import play.api.libs.json.Writes
import typo.dsl.Bijection

/** Type for the primary key of table `public.table-with-generated-columns` */
case class TableWithGeneratedColumnsId(value: String) extends AnyVal
object TableWithGeneratedColumnsId {
implicit lazy val arrayColumn: Column[Array[TableWithGeneratedColumnsId]] = Column.columnToArray(column, implicitly)
implicit lazy val arrayToStatement: ToStatement[Array[TableWithGeneratedColumnsId]] = ToStatement.arrayToParameter(ParameterMetaData.StringParameterMetaData).contramap(_.map(_.value))
implicit lazy val bijection: Bijection[TableWithGeneratedColumnsId, String] = Bijection[TableWithGeneratedColumnsId, String](_.value)(TableWithGeneratedColumnsId.apply)
implicit lazy val column: Column[TableWithGeneratedColumnsId] = Column.columnToString.map(TableWithGeneratedColumnsId.apply)
implicit lazy val ordering: Ordering[TableWithGeneratedColumnsId] = Ordering.by(_.value)
implicit lazy val parameterMetadata: ParameterMetaData[TableWithGeneratedColumnsId] = new ParameterMetaData[TableWithGeneratedColumnsId] {
override def sqlType: String = ParameterMetaData.StringParameterMetaData.sqlType
override def jdbcType: Int = ParameterMetaData.StringParameterMetaData.jdbcType
}
implicit lazy val reads: Reads[TableWithGeneratedColumnsId] = Reads.StringReads.map(TableWithGeneratedColumnsId.apply)
implicit lazy val text: Text[TableWithGeneratedColumnsId] = new Text[TableWithGeneratedColumnsId] {
override def unsafeEncode(v: TableWithGeneratedColumnsId, sb: StringBuilder) = Text.stringInstance.unsafeEncode(v.value, sb)
override def unsafeArrayEncode(v: TableWithGeneratedColumnsId, sb: StringBuilder) = Text.stringInstance.unsafeArrayEncode(v.value, sb)
}
implicit lazy val toStatement: ToStatement[TableWithGeneratedColumnsId] = ToStatement.stringToStatement.contramap(_.value)
implicit lazy val writes: Writes[TableWithGeneratedColumnsId] = Writes.StringWrites.contramap(_.value)
}
Original file line number Diff line number Diff line change
@@ -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 table_with_generated_columns

import java.sql.Connection
import typo.dsl.DeleteBuilder
import typo.dsl.SelectBuilder
import typo.dsl.UpdateBuilder

trait TableWithGeneratedColumnsRepo {
def delete: DeleteBuilder[TableWithGeneratedColumnsFields, TableWithGeneratedColumnsRow]
def deleteById(name: TableWithGeneratedColumnsId)(implicit c: Connection): Boolean
def deleteByIds(names: Array[TableWithGeneratedColumnsId])(implicit c: Connection): Int
def insert(unsaved: TableWithGeneratedColumnsRow)(implicit c: Connection): TableWithGeneratedColumnsRow
def insert(unsaved: TableWithGeneratedColumnsRowUnsaved)(implicit c: Connection): TableWithGeneratedColumnsRow
def insertStreaming(unsaved: Iterator[TableWithGeneratedColumnsRow], batchSize: Int = 10000)(implicit c: Connection): Long
/* NOTE: this functionality requires PostgreSQL 16 or later! */
def insertUnsavedStreaming(unsaved: Iterator[TableWithGeneratedColumnsRowUnsaved], batchSize: Int = 10000)(implicit c: Connection): Long
def select: SelectBuilder[TableWithGeneratedColumnsFields, TableWithGeneratedColumnsRow]
def selectAll(implicit c: Connection): List[TableWithGeneratedColumnsRow]
def selectById(name: TableWithGeneratedColumnsId)(implicit c: Connection): Option[TableWithGeneratedColumnsRow]
def selectByIds(names: Array[TableWithGeneratedColumnsId])(implicit c: Connection): List[TableWithGeneratedColumnsRow]
def selectByIdsTracked(names: Array[TableWithGeneratedColumnsId])(implicit c: Connection): Map[TableWithGeneratedColumnsId, TableWithGeneratedColumnsRow]
def update: UpdateBuilder[TableWithGeneratedColumnsFields, TableWithGeneratedColumnsRow]
def upsert(unsaved: TableWithGeneratedColumnsRow)(implicit c: Connection): TableWithGeneratedColumnsRow
def upsertBatch(unsaved: Iterable[TableWithGeneratedColumnsRow])(implicit c: Connection): List[TableWithGeneratedColumnsRow]
/* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */
def upsertStreaming(unsaved: Iterator[TableWithGeneratedColumnsRow], batchSize: Int = 10000)(implicit c: Connection): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* File has been automatically generated by `typo`.
*
* IF YOU CHANGE THIS FILE YOUR CHANGES WILL BE OVERWRITTEN.
*/
package adventureworks
package public
package table_with_generated_columns

import anorm.BatchSql
import anorm.NamedParameter
import anorm.ParameterValue
import anorm.RowParser
import anorm.SQL
import anorm.SimpleSql
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 TableWithGeneratedColumnsRepoImpl extends TableWithGeneratedColumnsRepo {
override def delete: DeleteBuilder[TableWithGeneratedColumnsFields, TableWithGeneratedColumnsRow] = {
DeleteBuilder(""""public"."table-with-generated-columns"""", TableWithGeneratedColumnsFields.structure)
}
override def deleteById(name: TableWithGeneratedColumnsId)(implicit c: Connection): Boolean = {
SQL"""delete from "public"."table-with-generated-columns" where "name" = ${ParameterValue(name, null, TableWithGeneratedColumnsId.toStatement)}""".executeUpdate() > 0
}
override def deleteByIds(names: Array[TableWithGeneratedColumnsId])(implicit c: Connection): Int = {
SQL"""delete
from "public"."table-with-generated-columns"
where "name" = ANY(${names})
""".executeUpdate()

}
override def insert(unsaved: TableWithGeneratedColumnsRow)(implicit c: Connection): TableWithGeneratedColumnsRow = {
SQL"""insert into "public"."table-with-generated-columns"("name")
values (${ParameterValue(unsaved.name, null, TableWithGeneratedColumnsId.toStatement)})
returning "name", "name-type-always"
"""
.executeInsert(TableWithGeneratedColumnsRow.rowParser(1).single)

}
override def insert(unsaved: TableWithGeneratedColumnsRowUnsaved)(implicit c: Connection): TableWithGeneratedColumnsRow = {
val namedParameters = List(
Some((NamedParameter("name", ParameterValue(unsaved.name, null, TableWithGeneratedColumnsId.toStatement)), ""))
).flatten
val quote = '"'.toString
if (namedParameters.isEmpty) {
SQL"""insert into "public"."table-with-generated-columns" default values
returning "name", "name-type-always"
"""
.executeInsert(TableWithGeneratedColumnsRow.rowParser(1).single)
} else {
val q = s"""insert into "public"."table-with-generated-columns"(${namedParameters.map{case (x, _) => quote + x.name + quote}.mkString(", ")})
values (${namedParameters.map{ case (np, cast) => s"{${np.name}}$cast"}.mkString(", ")})
returning "name", "name-type-always"
"""
SimpleSql(SQL(q), namedParameters.map { case (np, _) => np.tupled }.toMap, RowParser.successful)
.executeInsert(TableWithGeneratedColumnsRow.rowParser(1).single)
}

}
override def insertStreaming(unsaved: Iterator[TableWithGeneratedColumnsRow], batchSize: Int = 10000)(implicit c: Connection): Long = {
streamingInsert(s"""COPY "public"."table-with-generated-columns"("name") FROM STDIN""", batchSize, unsaved)(TableWithGeneratedColumnsRow.text, c)
}
/* NOTE: this functionality requires PostgreSQL 16 or later! */
override def insertUnsavedStreaming(unsaved: Iterator[TableWithGeneratedColumnsRowUnsaved], batchSize: Int = 10000)(implicit c: Connection): Long = {
streamingInsert(s"""COPY "public"."table-with-generated-columns"("name") FROM STDIN (DEFAULT '__DEFAULT_VALUE__')""", batchSize, unsaved)(TableWithGeneratedColumnsRowUnsaved.text, c)
}
override def select: SelectBuilder[TableWithGeneratedColumnsFields, TableWithGeneratedColumnsRow] = {
SelectBuilderSql(""""public"."table-with-generated-columns"""", TableWithGeneratedColumnsFields.structure, TableWithGeneratedColumnsRow.rowParser)
}
override def selectAll(implicit c: Connection): List[TableWithGeneratedColumnsRow] = {
SQL"""select "name", "name-type-always"
from "public"."table-with-generated-columns"
""".as(TableWithGeneratedColumnsRow.rowParser(1).*)
}
override def selectById(name: TableWithGeneratedColumnsId)(implicit c: Connection): Option[TableWithGeneratedColumnsRow] = {
SQL"""select "name", "name-type-always"
from "public"."table-with-generated-columns"
where "name" = ${ParameterValue(name, null, TableWithGeneratedColumnsId.toStatement)}
""".as(TableWithGeneratedColumnsRow.rowParser(1).singleOpt)
}
override def selectByIds(names: Array[TableWithGeneratedColumnsId])(implicit c: Connection): List[TableWithGeneratedColumnsRow] = {
SQL"""select "name", "name-type-always"
from "public"."table-with-generated-columns"
where "name" = ANY(${names})
""".as(TableWithGeneratedColumnsRow.rowParser(1).*)

}
override def selectByIdsTracked(names: Array[TableWithGeneratedColumnsId])(implicit c: Connection): Map[TableWithGeneratedColumnsId, TableWithGeneratedColumnsRow] = {
val byId = selectByIds(names).view.map(x => (x.name, x)).toMap
names.view.flatMap(id => byId.get(id).map(x => (id, x))).toMap
}
override def update: UpdateBuilder[TableWithGeneratedColumnsFields, TableWithGeneratedColumnsRow] = {
UpdateBuilder(""""public"."table-with-generated-columns"""", TableWithGeneratedColumnsFields.structure, TableWithGeneratedColumnsRow.rowParser)
}
override def upsert(unsaved: TableWithGeneratedColumnsRow)(implicit c: Connection): TableWithGeneratedColumnsRow = {
SQL"""insert into "public"."table-with-generated-columns"("name")
values (
${ParameterValue(unsaved.name, null, TableWithGeneratedColumnsId.toStatement)}
)
on conflict ("name")
do nothing
returning "name", "name-type-always"
"""
.executeInsert(TableWithGeneratedColumnsRow.rowParser(1).single)

}
override def upsertBatch(unsaved: Iterable[TableWithGeneratedColumnsRow])(implicit c: Connection): List[TableWithGeneratedColumnsRow] = {
def toNamedParameter(row: TableWithGeneratedColumnsRow): List[NamedParameter] = List(
NamedParameter("name", ParameterValue(row.name, null, TableWithGeneratedColumnsId.toStatement))
)
unsaved.toList match {
case Nil => Nil
case head :: rest =>
new anorm.adventureworks.ExecuteReturningSyntax.Ops(
BatchSql(
s"""insert into "public"."table-with-generated-columns"("name")
values ({name})
on conflict ("name")
do nothing
returning "name", "name-type-always"
""",
toNamedParameter(head),
rest.map(toNamedParameter)*
)
).executeReturning(TableWithGeneratedColumnsRow.rowParser(1).*)
}
}
/* NOTE: this functionality is not safe if you use auto-commit mode! it runs 3 SQL statements */
override def upsertStreaming(unsaved: Iterator[TableWithGeneratedColumnsRow], batchSize: Int = 10000)(implicit c: Connection): Int = {
SQL"""create temporary table table-with-generated-columns_TEMP (like "public"."table-with-generated-columns") on commit drop""".execute(): @nowarn
streamingInsert(s"""copy table-with-generated-columns_TEMP("name") from stdin""", batchSize, unsaved)(TableWithGeneratedColumnsRow.text, c): @nowarn
SQL"""insert into "public"."table-with-generated-columns"("name")
select * from table-with-generated-columns_TEMP
on conflict ("name")
do nothing
;
drop table table-with-generated-columns_TEMP;""".executeUpdate()
}
}
Loading

0 comments on commit dc03273

Please sign in to comment.