From 6d440e851089803e87f58ad09305b0e602802fdf Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sat, 23 Nov 2024 10:20:35 +0000 Subject: [PATCH 1/4] WIP --- .scalafmt.conf | 2 +- module-core/src/main/scala/roach/Query.scala | 10 ++ module-core/src/main/scala/roach/codec.scala | 54 +++++++-- .../main/scala/roach/sql_interpolator.scala | 107 ++++++++++++++++-- .../scala/roach/SqlInterpolatorTests.scala | 25 +++- 5 files changed, 173 insertions(+), 25 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index d402593..ef4948c 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.0" +version = "3.8.3" runner.dialect = scala3 rewrite.scala3.insertEndMarkerMinLines = 10 rewrite.scala3.removeOptionalBraces = true diff --git a/module-core/src/main/scala/roach/Query.scala b/module-core/src/main/scala/roach/Query.scala index 57ad073..20cecf1 100644 --- a/module-core/src/main/scala/roach/Query.scala +++ b/module-core/src/main/scala/roach/Query.scala @@ -3,6 +3,9 @@ package roach import scala.scalanative.unsafe.Zone import scala.util.NotGiven +// class Query: +// def all[T](using NotGiven[T]) + opaque type Query[T] = T => (Database, Zone) ?=> roach.Result object Query: def apply[T](q: String, codecIn: Codec[T]): Query[T] = @@ -11,6 +14,13 @@ object Query: def apply(q: String): Query[Unit] = data => (db, z) ?=> db.execute(q).getOrThrow + private[roach] def applyTransformed[Positional, UserSupplied]( + q: String, + codecIn: Codec[Positional], + transform: UserSupplied => Positional + ): Query[UserSupplied] = + data => (db, z) ?=> db.executeParams(q, codecIn, transform(data)).getOrThrow + extension [T](q: Query[T])(using NotGiven[T =:= Unit] ) diff --git a/module-core/src/main/scala/roach/codec.scala b/module-core/src/main/scala/roach/codec.scala index d7da65c..ef85202 100644 --- a/module-core/src/main/scala/roach/codec.scala +++ b/module-core/src/main/scala/roach/codec.scala @@ -39,24 +39,53 @@ trait ValueCodec[T] extends Codec[T]: end ValueCodec +private[roach] class TupleCodec[A, B](a: Codec[A], b: Codec[B]) + extends Codec[(A, B)]: + def accepts(offset: Int) = + if offset < a.length then a.accepts(offset) + else b.accepts(offset - a.length) + + def length = a.length + b.length + + def decode(get: Int => CString, isNull: Int => Boolean)(using Zone): (A, B) = + val left = a.decode(get, isNull) + val right = b.decode( + (i: Int) => get(i + a.length), + (i: Int) => isNull(i + a.length) + ) + (left, right) + + def encode(value: (A, B)) = + val (left, right) = value + val leftEncode = a.encode(left) + val rightEncode = b.encode(right) + + (offset: Int) => + if offset + 1 > a.length then rightEncode(offset - a.length) + else leftEncode(offset) + + override def toString() = + s"TupleCodec[$a, $b]" +end TupleCodec + private[roach] class AppendCodec[A <: Tuple, B](a: Codec[A], b: Codec[B]) - extends Codec[Tuple.Concat[A, (B *: EmptyTuple)]]: - type T = Tuple.Concat[A, (B *: EmptyTuple)] + extends Codec[Tuple.Append[A, B]]: + def accepts(offset: Int) = if offset < a.length then a.accepts(offset) else b.accepts(offset - a.length) def length = a.length + b.length - def decode(get: Int => CString, isNull: Int => Boolean)(using Zone): T = + def decode(get: Int => CString, isNull: Int => Boolean)(using Zone): Tuple.Append[A, B]= val left = a.decode(get, isNull) val right = b.decode( (i: Int) => get(i + a.length), (i: Int) => isNull(i + a.length) ) - left ++ (right *: EmptyTuple) + left :* right - def encode(value: T) = + def encode(value: Tuple.Append[A, B]) = val (left, right) = value.splitAt(a.length).asInstanceOf[(A, Tuple1[B])] val leftEncode = a.encode(left) val rightEncode = b.encode(right._1) @@ -67,12 +96,13 @@ private[roach] class AppendCodec[A <: Tuple, B](a: Codec[A], b: Codec[B]) override def toString() = s"AppendCodec[$a, $b]" - end AppendCodec -private[roach] class CombineCodec[A, B](a: Codec[A], b: Codec[B]) - extends Codec[(A, B)]: - type T = (A, B) +private[roach] class CombineCodec[A <: Tuple, B <: Tuple]( + a: Codec[A], + b: Codec[B] +) extends Codec[Tuple.Concat[A, B]]: + type T = Tuple.Concat[A, B] def accepts(offset: Int) = if offset < a.length then a.accepts(offset) else b.accepts(offset - a.length) @@ -83,11 +113,11 @@ private[roach] class CombineCodec[A, B](a: Codec[A], b: Codec[B]) val left = a.decode(get, isNull) val right = b.decode((i: Int) => get(i + a.length), (i: Int) => isNull(i + a.length)) - (left, right) + left ++ right def encode(value: T) = - val leftEncode = a.encode(value._1) - val rightEncode = b.encode(value._2) + val leftEncode = a.encode(value.take(a.length).asInstanceOf[A]) + val rightEncode = b.encode(value.drop(a.length).asInstanceOf[B]) (offset: Int) => if offset + 1 > a.length then rightEncode(offset - a.length) diff --git a/module-core/src/main/scala/roach/sql_interpolator.scala b/module-core/src/main/scala/roach/sql_interpolator.scala index c90249e..1d1c643 100644 --- a/module-core/src/main/scala/roach/sql_interpolator.scala +++ b/module-core/src/main/scala/roach/sql_interpolator.scala @@ -40,6 +40,8 @@ private[roach] object MacroImpl: val codecsBuilder = List.newBuilder[Expr[Any]] val segmentBuilders = List.newBuilder[Expr[Either[Int, String]]] + import quotes.reflect.* + args.foreach { case '{ $e: Codec[t] } => codecsBuilder.addOne(e) @@ -68,7 +70,7 @@ private[roach] object MacroImpl: val codecs = codecsBuilder.result() val insertions = Expr.ofList(segmentBuilders.result()) - val query = '{ + val queryString = '{ val parts = $strCtxExpr.parts.map(Option.apply) val ins = $insertions.map(Option.apply) @@ -94,21 +96,108 @@ private[roach] object MacroImpl: sb.result } + // REAL: "a" -> Int, "a" -> Int, Int, "b" -> String, Short, "b" -> String, "a" -> Int + // USER: "a" -> Int, Int, "b" -> String, Short, String + // mapping: 0 0 1 2 3 2 0 + codecs match - case Nil => '{ Query($query) } + case Nil => '{ Query($queryString) } case '{ $e: Codec[t] } :: Nil => - '{ Query[t]($query, $e) } - case h :: t :: _ => - val listOf = codecs.reduceLeft[Expr[Any]] { + '{ Query[t]($queryString, $e) } + case codecs @ (h :: rest) => + var seenAt = collection.mutable.Map.empty[String, Int] + val mappingB = List.newBuilder[Int] + + val enc = List.newBuilder[String] + + val originalCodecs = codecs.toArray + + var drift = 0 + var idx = 0 + codecs.foreach: expr => + val trueIndex = idx - drift + + enc += seenAt.toString() + " -- " + mappingB + .result() + .toString + "--" + expr.show + "\n" + expr match + case '{ $e: LabelledCodec[l, t] } => + val label = TypeRepr.of[l].show + + seenAt.get(label) match + case None => + mappingB += trueIndex + seenAt.update(label, trueIndex) + case Some(value) => + mappingB += value + drift += 1 + case '{ $e: Codec[t] } => + mappingB += trueIndex + end match + + idx += 1 + + val mapping = mappingB.result() + val compressedCodecs = Array.ofDim[Expr[Any]](mapping.max + 1) + + originalCodecs.zipWithIndex.foreach: (codec, idx) => + val mapTo = mapping(idx) + // println(s"Mapping ${codec.show} from $idx to $mapTo") + compressedCodecs(mapTo) = codec + + val remap = Expr( + mapping.zipWithIndex + .map(_.swap) + .groupMapReduce(_._2)(s => List(s._1))(_ ++ _) + ) + val map = Expr(mapping) + // quotes.reflect.report.info( + // enc.result().toString + "-->" + + // mapping.toString + "-->" + + // originalCodecs.toList + // .map(_.show) + // .toString() + " --> " + compressedCodecs.toList + // .map(_.show) + // .toString() + " --> " + remap.show + // ) + + // val remappingFunction + + // quotes.reflect.report.info(mapping.result().toString()) + // quotes.reflect.report.info(seenAt.toString()) + // quotes.reflect.report.info(enc.result().toString() + " -- " + mapping) + + val original = codecs.reduceLeft[Expr[Any]] { + case ('{ $e: Codec[t] }, '{ $e1: Codec[t1] }) => + '{ $e *: $e1 } + } + + val userSupplied = compressedCodecs.reduceLeft[Expr[Any]] { case ('{ $e: Codec[t] }, '{ $e1: Codec[t1] }) => - '{ $e ~ $e1 } + '{ $e ~ $e1) } } - listOf match - case '{ $e: Codec[t] } => + (userSupplied, original) match + case ('{ $e: Codec[userSuppliedT] }, '{ $e1: Codec[positionalT] }) => '{ - Query[t]($query, $e) + val transform: userSuppliedT => positionalT = us => + val usTuple = us.asInstanceOf[Tuple].toArray + val positional = Array.ofDim[Any]($map.length) + var start = 0 + while start < usTuple.length do + val mapTo = $remap(start) + mapTo.foreach: idx => + positional(idx) = usTuple(idx) + start += 1 + + Tuple.fromArray(positional).asInstanceOf[positionalT] + Query.applyTransformed[positionalT, userSuppliedT]( + $queryString, + $e1, + transform + ) } + end match end match end sql diff --git a/module-core/src/test/scala/roach/SqlInterpolatorTests.scala b/module-core/src/test/scala/roach/SqlInterpolatorTests.scala index 481851b..2718bd5 100644 --- a/module-core/src/test/scala/roach/SqlInterpolatorTests.scala +++ b/module-core/src/test/scala/roach/SqlInterpolatorTests.scala @@ -18,11 +18,30 @@ class SqlInterpolatorTests extends munit.FunSuite, TestHarness: Zone { withDB { val fr1 = Fragment("key,value") - val q = sql"select $fr1 from $tableName where key = 25".all(int4 ~ text) + val b = sql"select $text, $int2 from table" + + // type T = ("label1", Int) *: ("label2", Int) *: String *: Short *: EmptyTuple + locally: + val a = int4.label["a"] + val b = text.label["b"] + + val ql = sql"""select * from posts + where author_id = $a + or repost_author_id = $a + or x = $int4 + or b = $b + or x = $int2 + or z = $b + or a = $a + """ + + // ql.count(("label1" -> 25, "label2" -> 50, "hello", 25)) + + val q = sql"select $fr1 from $tableName where key = 25".all(int4 *: text) assertEquals(q, Vector(25 -> "hello")) - val fr2 = fr1.applied(int4 ~ text) + val fr2 = fr1.applied(int4 *: text) val q1 = sql"insert into $tableName(${fr2.sql}) values ($fr2) returning key" @@ -39,7 +58,7 @@ class SqlInterpolatorTests extends munit.FunSuite, TestHarness: withDB { case class Data(key: Int, value: String) - val rc = (int4 ~ text).as[Data] + val rc = (int4 *: text).as[Data] sql"insert into $tableName values ($rc)".exec(Data(150, "howdies")) assertEquals( From c0b8a0265bbe5f68c83734da17e26ebe9b7ec14c Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sat, 23 Nov 2024 12:22:22 +0000 Subject: [PATCH 2/4] Tests and fixes --- build.sbt | 3 +- module-core/src/main/scala/roach/Query.scala | 124 +++++++++++------- module-core/src/main/scala/roach/codec.scala | 46 +++++-- .../main/scala/roach/sql_interpolator.scala | 50 +++---- .../scala/roach/SqlInterpolatorTests.scala | 91 +++++++++---- 5 files changed, 202 insertions(+), 112 deletions(-) diff --git a/build.sbt b/build.sbt index 144d14d..1f10f6b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,3 +1,4 @@ +import scala.scalanative.build.SourceLevelDebuggingConfig import bindgen.plugin.BindgenMode import java.nio.file.Paths @@ -32,7 +33,7 @@ lazy val core = ) .settings(common) .settings( - Test / nativeConfig ~= (_.withEmbedResources(true)), + Test / nativeConfig ~= (_.withEmbedResources(true).withSourceLevelDebuggingConfig(SourceLevelDebuggingConfig.enabled)), Compile / bindgenBindings += { val configurator = vcpkgConfigurator.value diff --git a/module-core/src/main/scala/roach/Query.scala b/module-core/src/main/scala/roach/Query.scala index 20cecf1..1384e76 100644 --- a/module-core/src/main/scala/roach/Query.scala +++ b/module-core/src/main/scala/roach/Query.scala @@ -6,63 +6,87 @@ import scala.util.NotGiven // class Query: // def all[T](using NotGiven[T]) -opaque type Query[T] = T => (Database, Zone) ?=> roach.Result +trait Query[T]: + self => + protected def execute(data: T)(using db: Database, z: Zone): roach.Result + + def contramap[X](transform: X => T): Query[X] = + new Query[X]: + override protected def execute( + data: X + )(using db: Database, z: Zone): Result = self.execute(transform(data)) + + override def query: String = self.query + + def all[X](data: T, codec: Codec[X])(using + iz: Zone, + db: Database + ): Vector[X] = + execute(data).use(_.readAll(codec)) + + def execute[X](data: T, codec: Codec[X])(using + iz: Zone, + db: Database + ): Option[X] = + execute(data).use(_.readOne(codec)) + + def exec(data: T)(using + iz: Zone, + db: Database + ): Unit = + execute(data).use { _ => } + + def count(data: T)(using + iz: Zone, + db: Database + ): Int = + execute(data).use(_.count()) + + def one[X](data: T, codec: Codec[X])(using + iz: Zone, + db: Database + ): Option[X] = + execute(data).use(_.readOne(codec)) + + def query: String +end Query + +private class QueryImpl[T](val query: String, codec: Codec[T]) extends Query[T]: + protected def execute(data: T)(using db: Database, z: Zone): roach.Result = + db.executeParams[T](query, codec, data).getOrThrow + +class VoidQuery(str: String): + private def execute()(using db: Database, z: Zone): roach.Result = + db.execute(str).getOrThrow + + def all[X](codec: Codec[X])(using iz: Zone, db: Database): Vector[X] = + execute().use(_.readAll(codec)(using iz)) + + def one[X](codec: Codec[X])(using iz: Zone, db: Database): Option[X] = + execute().use(_.readOne(codec)(using iz)) + + def exec()(using iz: Zone, db: Database): Unit = + execute().use { _ => } + + def count()(using + iz: Zone, + db: Database + ): Int = + execute().use(_.count()) + +end VoidQuery + object Query: def apply[T](q: String, codecIn: Codec[T]): Query[T] = - data => (db, z) ?=> db.executeParams[T](q, codecIn, data).getOrThrow + new QueryImpl(q, codecIn) - def apply(q: String): Query[Unit] = - data => (db, z) ?=> db.execute(q).getOrThrow + def apply(q: String): VoidQuery = + new VoidQuery(q) private[roach] def applyTransformed[Positional, UserSupplied]( q: String, codecIn: Codec[Positional], transform: UserSupplied => Positional ): Query[UserSupplied] = - data => (db, z) ?=> db.executeParams(q, codecIn, transform(data)).getOrThrow - - extension [T](q: Query[T])(using - NotGiven[T =:= Unit] - ) - def all[X](data: T, codec: Codec[X])(using - iz: Zone, - db: Database - ): Vector[X] = - q(data).use(_.readAll(codec)(using iz)) - - def one[X](data: T, codec: Codec[X])(using - iz: Zone, - db: Database - ): Option[X] = - q(data).use(_.readOne(codec)(using iz)) - - def exec(data: T)(using - iz: Zone, - db: Database - ): Unit = - q(data).use { _ => } - - def count(data: T)(using - iz: Zone, - db: Database - ): Int = - q(data).use(_.count()) - end extension - - extension (q: Query[Unit]) - def all[X](codec: Codec[X])(using iz: Zone, db: Database): Vector[X] = - q(()).use(_.readAll(codec)(using iz)) - - def one[X](codec: Codec[X])(using iz: Zone, db: Database): Option[X] = - q(()).use(_.readOne(codec)(using iz)) - - def exec()(using iz: Zone, db: Database): Unit = - q(()).use { _ => } - - def count()(using - iz: Zone, - db: Database - ): Int = - q(()).use(_.count()) - end extension + apply(q, codecIn).contramap(transform) end Query diff --git a/module-core/src/main/scala/roach/codec.scala b/module-core/src/main/scala/roach/codec.scala index ef85202..b0d89e4 100644 --- a/module-core/src/main/scala/roach/codec.scala +++ b/module-core/src/main/scala/roach/codec.scala @@ -39,6 +39,27 @@ trait ValueCodec[T] extends Codec[T]: end ValueCodec +private[roach] class AutoTupledCodec[A](a: Codec[A]) + extends Codec[A *: EmptyTuple]: + + def accepts(offset: Int) = a.accepts(offset) + + def length = a.length + + def decode(get: Int => CString, isNull: Int => Boolean)(using + Zone + ): Tuple1[A] = + val left = a.decode(get, isNull) + + Tuple1(left) + + override def encode(value: A *: EmptyTuple) = + a.encode(value._1) + + override def toString() = + s"AutoTupledCodec[$a]" +end AutoTupledCodec + private[roach] class TupleCodec[A, B](a: Codec[A], b: Codec[B]) extends Codec[(A, B)]: def accepts(offset: Int) = @@ -69,24 +90,25 @@ private[roach] class TupleCodec[A, B](a: Codec[A], b: Codec[B]) end TupleCodec private[roach] class AppendCodec[A <: Tuple, B](a: Codec[A], b: Codec[B]) - extends Codec[Tuple.Append[A, B]]: - + extends Codec[Tuple.Concat[A, (B *: EmptyTuple)]]: + type T = Tuple.Concat[A, (B *: EmptyTuple)] def accepts(offset: Int) = if offset < a.length then a.accepts(offset) else b.accepts(offset - a.length) def length = a.length + b.length - def decode(get: Int => CString, isNull: Int => Boolean)(using Zone): Tuple.Append[A, B]= + def decode(get: Int => CString, isNull: Int => Boolean)(using Zone): T = val left = a.decode(get, isNull) val right = b.decode( (i: Int) => get(i + a.length), (i: Int) => isNull(i + a.length) ) - left :* right + left ++ (right *: EmptyTuple) - def encode(value: Tuple.Append[A, B]) = - val (left, right) = value.splitAt(a.length).asInstanceOf[(A, Tuple1[B])] + def encode(value: T) = + val (left, right) = + value.splitAt(value.size - 1).asInstanceOf[(A, B *: EmptyTuple)] val leftEncode = a.encode(left) val rightEncode = b.encode(right._1) @@ -96,6 +118,7 @@ private[roach] class AppendCodec[A <: Tuple, B](a: Codec[A], b: Codec[B]) override def toString() = s"AppendCodec[$a, $b]" + end AppendCodec private[roach] class CombineCodec[A <: Tuple, B <: Tuple]( @@ -147,11 +170,15 @@ object Codec: def encode(value: T) = d.encode(iso.invert(value)) + override def toString(): String = s"IsoCodec[$d, $iso]" + extension [A](d: Codec[A]) inline def ~[B]( other: Codec[B] )(using NotGiven[B <:< Tuple]): Codec[(A, B)] = - CombineCodec(d, other) + TupleCodec(d, other) + // AppendCodec(AutoTupledCodec(d), other) + end extension def stringLike[A]( @@ -167,7 +194,7 @@ object Codec: def encode(value: A) = _ => toCString(g(value)) - override def toString() = s"ValueCodec[$accept]" + override def toString() = s"$accept" end Codec @@ -183,3 +210,6 @@ object Iso: mir.fromProduct(a) def invert(a: A) = Tuple.fromProduct(a.asInstanceOf[Product]).asInstanceOf[X] + + override def toString(): String = "Iso[" + mir.toString + "]" +end Iso diff --git a/module-core/src/main/scala/roach/sql_interpolator.scala b/module-core/src/main/scala/roach/sql_interpolator.scala index 1d1c643..ffe64ef 100644 --- a/module-core/src/main/scala/roach/sql_interpolator.scala +++ b/module-core/src/main/scala/roach/sql_interpolator.scala @@ -96,10 +96,6 @@ private[roach] object MacroImpl: sb.result } - // REAL: "a" -> Int, "a" -> Int, Int, "b" -> String, Short, "b" -> String, "a" -> Int - // USER: "a" -> Int, Int, "b" -> String, Short, String - // mapping: 0 0 1 2 3 2 0 - codecs match case Nil => '{ Query($queryString) } case '{ $e: Codec[t] } :: Nil => @@ -142,7 +138,6 @@ private[roach] object MacroImpl: originalCodecs.zipWithIndex.foreach: (codec, idx) => val mapTo = mapping(idx) - // println(s"Mapping ${codec.show} from $idx to $mapTo") compressedCodecs(mapTo) = codec val remap = Expr( @@ -151,31 +146,24 @@ private[roach] object MacroImpl: .groupMapReduce(_._2)(s => List(s._1))(_ ++ _) ) val map = Expr(mapping) - // quotes.reflect.report.info( - // enc.result().toString + "-->" + - // mapping.toString + "-->" + - // originalCodecs.toList - // .map(_.show) - // .toString() + " --> " + compressedCodecs.toList - // .map(_.show) - // .toString() + " --> " + remap.show - // ) - - // val remappingFunction - - // quotes.reflect.report.info(mapping.result().toString()) - // quotes.reflect.report.info(seenAt.toString()) - // quotes.reflect.report.info(enc.result().toString() + " -- " + mapping) - - val original = codecs.reduceLeft[Expr[Any]] { - case ('{ $e: Codec[t] }, '{ $e1: Codec[t1] }) => - '{ $e *: $e1 } - } + def combineCodecs(codecs: List[Expr[Any]]): Expr[Any] = + codecs match + case head :: Nil => + head + case '{ $h: Codec[t] } :: '{ $r: Codec[t1] } :: rest => + rest.foldLeft[Expr[Any]]('{ TupleCodec($h, $r) }): + case ('{ $acc: TupleCodec[a, z] }, '{ $next: Codec[t] }) => + '{ AppendCodec($acc, $next) } + case ('{ $acc: AppendCodec[a, z] }, '{ $next: Codec[t] }) => + '{ AppendCodec($acc, $next) } + case (other, next) => + quotes.reflect.report.errorAndAbort(other.show) - val userSupplied = compressedCodecs.reduceLeft[Expr[Any]] { - case ('{ $e: Codec[t] }, '{ $e1: Codec[t1] }) => - '{ $e ~ $e1) } - } + end combineCodecs + + val original = combineCodecs(codecs) + + val userSupplied = combineCodecs(compressedCodecs.toList) (userSupplied, original) match case ('{ $e: Codec[userSuppliedT] }, '{ $e1: Codec[positionalT] }) => @@ -187,10 +175,12 @@ private[roach] object MacroImpl: while start < usTuple.length do val mapTo = $remap(start) mapTo.foreach: idx => - positional(idx) = usTuple(idx) + positional(idx) = usTuple(start) start += 1 Tuple.fromArray(positional).asInstanceOf[positionalT] + end transform + Query.applyTransformed[positionalT, userSuppliedT]( $queryString, $e1, diff --git a/module-core/src/test/scala/roach/SqlInterpolatorTests.scala b/module-core/src/test/scala/roach/SqlInterpolatorTests.scala index 2718bd5..4901270 100644 --- a/module-core/src/test/scala/roach/SqlInterpolatorTests.scala +++ b/module-core/src/test/scala/roach/SqlInterpolatorTests.scala @@ -14,6 +14,9 @@ class SqlInterpolatorTests extends munit.FunSuite, TestHarness: insert into $tableName values(42, 'bye'); """) + case class Data(key: Int, value: String) + val rc = (int4 ~ text).as[Data] + test("fragments") { Zone { withDB { @@ -21,27 +24,11 @@ class SqlInterpolatorTests extends munit.FunSuite, TestHarness: val b = sql"select $text, $int2 from table" // type T = ("label1", Int) *: ("label2", Int) *: String *: Short *: EmptyTuple - locally: - val a = int4.label["a"] - val b = text.label["b"] - - val ql = sql"""select * from posts - where author_id = $a - or repost_author_id = $a - or x = $int4 - or b = $b - or x = $int2 - or z = $b - or a = $a - """ - - // ql.count(("label1" -> 25, "label2" -> 50, "hello", 25)) - - val q = sql"select $fr1 from $tableName where key = 25".all(int4 *: text) + val q = sql"select $fr1 from $tableName where key = 25".all(int4 ~ text) assertEquals(q, Vector(25 -> "hello")) - val fr2 = fr1.applied(int4 *: text) + val fr2 = fr1.applied(int4 ~ text) val q1 = sql"insert into $tableName(${fr2.sql}) values ($fr2) returning key" @@ -53,23 +40,81 @@ class SqlInterpolatorTests extends munit.FunSuite, TestHarness: } } + test("labelled queries") { + Zone: + withDB: + val key1 = int4.label["key1"] + val key2 = int4.label["key2"] + val value1 = text.label["value1"] + val value2 = text.label["value2"] + + val ql = sql""" + select * from $tableName + where + (key = $key1 and value = $value1) OR + (key = 312 and value = $text) OR + (key = $key2 and value = $value2) OR + (key = $key1 and value = 'test') OR + (key = $int4 and value = $text) OR + (key = $key2 and value = 'test') + """ + val rc = int4 ~ text + val insert = + sql"insert into $tableName values ($rc), ($rc), ($rc), ($rc), ($rc), ($rc)" + + insert.exec( + ( + (150, "hello"), + (256, "world"), + (150, "test"), + (256, "test"), + (312, "test2"), + (25, "yo") + ) + ) + + val expected = Vector( + (150, "hello"), + (256, "world"), + (150, "test"), + (256, "test"), + (312, "test2"), + (25, "yo") + ) + + assertEquals( + ql.all( + ( + "key1" -> 150, + "value1" -> "hello", + "test2", + "key2" -> 256, + "value2" -> "world", + 25, + "yo" + ), + rc + ), + expected + ) + + } + test("basics") { Zone { withDB { - case class Data(key: Int, value: String) - val rc = (int4 *: text).as[Data] - sql"insert into $tableName values ($rc)".exec(Data(150, "howdies")) + sql"insert into $tableName values ($rc)".exec(Data(1500, "howdies")) assertEquals( sql"select * from $tableName where key = $int4 and value = $text" .count( - 150 -> "howdies" + 1500 -> "howdies" ), 1 ) assertEquals( - sql"select value from $tableName where key = $int4".one(150, text), + sql"select value from $tableName where key = $int4".one(1500, text), Some("howdies") ) From 74fd47b193b40a549eee6d8dabbb7c3fd8499c00 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sat, 23 Nov 2024 12:30:48 +0000 Subject: [PATCH 3/4] labelled codec --- .../src/main/scala/roach/LabelledCodec.scala | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 module-core/src/main/scala/roach/LabelledCodec.scala diff --git a/module-core/src/main/scala/roach/LabelledCodec.scala b/module-core/src/main/scala/roach/LabelledCodec.scala new file mode 100644 index 0000000..7a28c77 --- /dev/null +++ b/module-core/src/main/scala/roach/LabelledCodec.scala @@ -0,0 +1,27 @@ +package roach + +import scala.scalanative.unsafe.CString + +import scala.ContextFunction1 + +import scala.scalanative.unsafe.Zone + +class LabelledCodec[L <: Singleton & String, T](lab: L, c: Codec[T]) extends Codec[(L, T)]: + override def length: Int = c.length + + override def decode(get: Int => CString, isNull: Int => Boolean)(using + Zone + ): (L, T) = + lab -> c.decode(get, isNull) + + override def encode(value: (L, T)): Int => (Zone) ?=> CString = + c.encode(value._2) + + export c.accepts + + override def toString(): String = s"LabelledCodec[$lab, $c]" +end LabelledCodec + +extension [T](c: Codec[T]) + inline def label[L <: Singleton & String]: LabelledCodec[L, T] = + LabelledCodec(compiletime.constValue[L], c) From f0e0b8518fd9c545bfc2dce28a6882d994c04e4f Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sat, 23 Nov 2024 12:55:14 +0000 Subject: [PATCH 4/4] label codec docs --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f9c9c95..cb84ec3 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,15 @@ def example(using Database, Zone) = sql"select count(*) from my_table".count() sql"select count(*) from my_table where x = $int4 and y = $text".count(25 -> "hello") + // Label codecs for clarity + val id = text.label["id"] + val name = text.label["name"] + + sql"select count(*) from my_table where id = $id or name = $name or age = $int4".one( + ("id" -> "id_1", "name" -> "tony", 50), + int4 + ) + case class Data(key: Int, value: String) val rc = (int4 ~ text).as[Data] val tableName = "my_table" @@ -118,7 +127,7 @@ def example_failure(using Database, Zone) = sql"select count(*) from my_table where x = $int4 and y = $text".count("hello" -> 25) ``` -`sql` interpolator produces a `Query`, so you can use it as described in the previous section +`sql` interpolator produces a `Query`, so you can use it as described in the previous section. ### Fragments