Expand typed SQL DSL
This commit is contained in:
parent
72606e7818
commit
314c8fb207
17 changed files with 1082 additions and 498 deletions
822
README.md
822
README.md
|
|
@ -1,115 +1,143 @@
|
|||
# Gotlin
|
||||
|
||||
`Gotlin` is a small Kotlin-like frontend implemented in Go that targets the Go toolchain.
|
||||
|
||||
This is the practical boundary of the prototype:
|
||||
|
||||
- It is a Kotlin-flavored language frontend.
|
||||
- It targets the Go toolchain by generating valid Go source and building through `go build`.
|
||||
- It is not a direct integration into Go's internal `cmd/compile` backend APIs.
|
||||
|
||||
The compiler keeps source spelling in its syntax AST, then builds lexical
|
||||
symbols, structural semantic types, resolved expression meanings, and typed
|
||||
HIR before Go emission. Class reference semantics live in `ClassType`; only
|
||||
the semantic Type-to-Go mapping turns a class such as `User` into `*User`.
|
||||
Separately compiled Gotlin packages publish a versioned `.gti.json` interface;
|
||||
imports load that interface before falling back to `go/types`, preserving class
|
||||
reference semantics, generics, enums, function signatures, and inferred effects
|
||||
across package boundaries.
|
||||
|
||||
## Supported language slice
|
||||
|
||||
- `fun` declarations
|
||||
- `val` and `var`
|
||||
- `Int`, `Long`, `String`, `Boolean`, `Unit`
|
||||
- function types like `(String) -> Unit`
|
||||
- user generic functions and classes with inferred or explicit type arguments
|
||||
- `if`, `else`, `while`, and `for (item in items)`
|
||||
- function calls
|
||||
- lambdas like `{ x: Int -> println(x) }` and `{ println(it) }`
|
||||
- Kotlin-style final-expression returns in value lambdas, for example `{ value -> value * 2 }`
|
||||
- `class` with primary-constructor fields and methods
|
||||
- `interface` with method signatures
|
||||
- Rust-style algebraic `enum` declarations with payload variants and exhaustive `match`
|
||||
- `println(...)`
|
||||
- arithmetic, comparison, and boolean operators
|
||||
- decimal literals, `defer`, and Go address-of expressions such as `&value`
|
||||
- explicit nullable types (`Type?`), safe access (`?.`), and non-null assertions (`!!`)
|
||||
- Gotlin classes are reference types by default; `*` is only needed for external Go pointer types
|
||||
- named external Go struct construction, for example `http.Client(timeout = 3 * time.second)`
|
||||
- top-level embedded resources such as `@embed("assets/*") val assets: embed.FS`
|
||||
- inferred structured coroutine effects with `runBlocking`, `coroutineScope`, `launch`, `async`, `await`, `delay`, `withTimeout`, `isActive`, and `coroutineContext`
|
||||
|
||||
## Example
|
||||
Gotlin is a Kotlin-flavored language implemented in Go. It compiles typed
|
||||
Gotlin source to Go and uses the Go toolchain for binaries, packages, and
|
||||
interop.
|
||||
|
||||
```kotlin
|
||||
package demo
|
||||
package main
|
||||
|
||||
fun fib(n: Int): Int {
|
||||
if (n < 2) {
|
||||
return n
|
||||
}
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
fun fibonacci(value: Int): Int {
|
||||
if (value < 2) { return value }
|
||||
return fibonacci(value - 1) + fibonacci(value - 2)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(fib(8))
|
||||
println(fibonacci(10))
|
||||
}
|
||||
```
|
||||
|
||||
Imports from Go packages are supported:
|
||||
## Compiler pipeline
|
||||
|
||||
```kotlin
|
||||
package demo
|
||||
|
||||
import strings
|
||||
|
||||
fun main() {
|
||||
println(strings.ToUpper("gotlin"))
|
||||
}
|
||||
```text
|
||||
Lexer
|
||||
-> parser and syntax AST
|
||||
-> package resolver and structural TypeRef resolution
|
||||
-> lexical symbols, type checking, effects, and diagnostics
|
||||
-> typed HIR
|
||||
-> Go emission
|
||||
-> Go compiler
|
||||
```
|
||||
|
||||
HTTP server example:
|
||||
Semantic types distinguish named values, Gotlin classes, nullable values, Go
|
||||
pointers, functions, generics, tuples, and imported Gotlin classes. A Gotlin
|
||||
class remains `User` throughout semantic analysis; only Type-to-Go lowering
|
||||
chooses the `*User` representation.
|
||||
|
||||
Separately compiled Gotlin packages expose a versioned `.gti.json` interface
|
||||
containing classes, enums, signatures, generics, and inferred effects. The
|
||||
compiler and LSP load package interfaces before using `go/types` for ordinary
|
||||
Go dependencies.
|
||||
|
||||
## Functions and values
|
||||
|
||||
```kotlin
|
||||
package demo.web
|
||||
|
||||
import fmt
|
||||
import net.http
|
||||
|
||||
fun helloHandler(w: http.ResponseWriter, r: *http.Request) {
|
||||
fmt.Fprintln(w, "hello from gotlin")
|
||||
fun greet(name: String): String {
|
||||
val message = "Hello " + name
|
||||
return message
|
||||
}
|
||||
|
||||
fun main() {
|
||||
http.HandleFunc("/", helloHandler)
|
||||
fmt.Println("serving http://localhost:8080")
|
||||
http.ListenAndServe(":8080", http.DefaultServeMux)
|
||||
}
|
||||
fun doubled(value: Int) = value * 2
|
||||
```
|
||||
|
||||
Classes and interfaces:
|
||||
`val` is immutable and `var` is mutable:
|
||||
|
||||
```kotlin
|
||||
package demo
|
||||
val accountId = "account-1"
|
||||
var attempts = 0
|
||||
attempts += 1
|
||||
```
|
||||
|
||||
Supported control flow includes `if`, `else`, `while`, `for`, `try`, `catch`,
|
||||
`throw`, exhaustive `match`, and `defer`.
|
||||
|
||||
## Classes and interfaces
|
||||
|
||||
Gotlin classes are reference-valued by default:
|
||||
|
||||
```kotlin
|
||||
interface Greeter {
|
||||
fun greet(name: String): String
|
||||
}
|
||||
|
||||
class ConsoleGreeter(val prefix: String) {
|
||||
fun greet(name: String): String {
|
||||
return prefix + name
|
||||
}
|
||||
class PrefixGreeter(val prefix: String): Greeter {
|
||||
fun greet(name: String) = prefix + name
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val greeter: Greeter = ConsoleGreeter("hello, ")
|
||||
println(greeter.greet("gotlin"))
|
||||
fun create(): PrefixGreeter = PrefixGreeter("Hello ")
|
||||
```
|
||||
|
||||
Use `*` only for Go pointer types:
|
||||
|
||||
```kotlin
|
||||
fun handle(request: *http.Request, pool: *pgxpool.Pool) { }
|
||||
```
|
||||
|
||||
Imported Gotlin classes retain reference semantics across package boundaries:
|
||||
|
||||
```kotlin
|
||||
fun publish(lifecycle: platform.Lifecycle) { }
|
||||
```
|
||||
|
||||
## Null safety
|
||||
|
||||
Types are non-nullable unless marked with `?`:
|
||||
|
||||
```kotlin
|
||||
fun email(user: User?): String {
|
||||
if (user == null) { return "missing" }
|
||||
return user.email
|
||||
}
|
||||
```
|
||||
|
||||
Rust-style enums:
|
||||
Safe access and non-null assertions are available:
|
||||
|
||||
```kotlin
|
||||
val city: String? = user?.address?.city
|
||||
val required: User = optionalUser!!
|
||||
```
|
||||
|
||||
The semantic analyzer smart-casts values after null checks and guard clauses.
|
||||
|
||||
## Data classes and JSON
|
||||
|
||||
```kotlin
|
||||
@jsonNaming(camelCase)
|
||||
data class AccountReply(
|
||||
var accountId: String,
|
||||
var availableBalance: Double
|
||||
)
|
||||
```
|
||||
|
||||
Supported JSON policies are `snakeCase`, `camelCase`, `pascalCase`, and
|
||||
`kebabCase`.
|
||||
|
||||
```kotlin
|
||||
val body = json.marshal(reply).unwrap()
|
||||
val decoded = json.decode<AccountReply>(body).unwrap()
|
||||
```
|
||||
|
||||
Resources can be embedded at package scope:
|
||||
|
||||
```kotlin
|
||||
import embed
|
||||
|
||||
@embed("static/*") val assets: embed.FS
|
||||
```
|
||||
|
||||
## Enums and match
|
||||
|
||||
Enums support payloadless and payload variants:
|
||||
|
||||
```kotlin
|
||||
enum PaymentResult {
|
||||
|
|
@ -117,196 +145,92 @@ enum PaymentResult {
|
|||
Rejected(String)
|
||||
Pending
|
||||
}
|
||||
```
|
||||
|
||||
fun describe(result: PaymentResult): String {
|
||||
return match (result) {
|
||||
PaymentResult.Accepted(id) -> id
|
||||
PaymentResult.Rejected(reason) -> reason
|
||||
PaymentResult.Pending -> "pending"
|
||||
Matches are exhaustive and may return values:
|
||||
|
||||
```kotlin
|
||||
fun description(result: PaymentResult) = match (result) {
|
||||
PaymentResult.Accepted(id) -> "accepted " + id
|
||||
PaymentResult.Rejected(reason) -> "rejected " + reason
|
||||
PaymentResult.Pending -> "pending"
|
||||
}
|
||||
```
|
||||
|
||||
Payloadless enums are represented as exact string-backed values, making them
|
||||
suitable for JSON and PostgreSQL columns.
|
||||
|
||||
## Result error handling
|
||||
|
||||
```kotlin
|
||||
fun parse(value: String): Result<Int, Error> = strconv.atoi(value)
|
||||
|
||||
fun doubled(value: String): Result<Int, Error> {
|
||||
val parsed = strconv.atoi(value)?
|
||||
return Result.Ok(parsed * 2)
|
||||
}
|
||||
```
|
||||
|
||||
Available operations include `?`, `unwrap()`, `unwrapOr(value)`, explicit
|
||||
destructuring, and exhaustive `Result` matching. Go `(T, error)` and error-only
|
||||
returns adapt to `Result` when required by context.
|
||||
|
||||
## Generics and lambdas
|
||||
|
||||
```kotlin
|
||||
data class Box<T>(var value: T) {
|
||||
fun get(): T = value
|
||||
}
|
||||
|
||||
fun identity<T>(value: T): T = value
|
||||
|
||||
val inferred = identity(42)
|
||||
val explicit = identity<String>("value")
|
||||
val boxed = Box("text")
|
||||
```
|
||||
|
||||
Higher-order functions use Kotlin-style function types and trailing lambdas:
|
||||
|
||||
```kotlin
|
||||
fun transform<T, R>(value: T, block: (T) -> R): R = block(value)
|
||||
|
||||
val answer = transform(21) { value ->
|
||||
value * 2
|
||||
}
|
||||
```
|
||||
|
||||
The final expression is returned automatically for value lambdas. Unit lambdas
|
||||
execute their final expression as a statement. Explicit `return` is also
|
||||
supported.
|
||||
|
||||
## Structured concurrency and context
|
||||
|
||||
Coroutine effects are inferred from direct and transitive calls:
|
||||
|
||||
```kotlin
|
||||
fun poll() {
|
||||
while (isActive()) {
|
||||
receiveEvents()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
runBlocking {
|
||||
launch { poll() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enum matches must contain each variant exactly once. Variant payload arity is
|
||||
checked during Gotlin compilation. A match used as an expression also requires
|
||||
every arm to return the same type. Block-style statement matches remain
|
||||
available for side effects.
|
||||
`launch` creates a structured Unit child. `async<T>` creates a typed deferred
|
||||
child. Scopes wait for children, propagate failures, and cancel siblings.
|
||||
|
||||
## User generics
|
||||
Available operations include `runBlocking`, `withContext`, `coroutineScope`,
|
||||
`launch`, `async`, `await`, `delay`, `withTimeout`, `isActive`, and
|
||||
`coroutineContext`.
|
||||
|
||||
Functions and classes may declare type parameters. Calls infer straightforward
|
||||
type bindings from arguments or accept explicit type arguments:
|
||||
|
||||
```kotlin
|
||||
data class Box<T>(var value: T) {
|
||||
fun get(): T { return value }
|
||||
}
|
||||
|
||||
fun identity<T>(value: T): T { return value }
|
||||
|
||||
val number = identity(42)
|
||||
val text = identity<String>("value")
|
||||
val box = Box("boxed")
|
||||
```
|
||||
|
||||
Type parameters currently use an implicit `Any` constraint. Generic methods
|
||||
with their own type parameters are intentionally deferred; place parameters on
|
||||
the enclosing class or a top-level function.
|
||||
|
||||
## Null safety
|
||||
|
||||
Types are non-nullable by default. Add `?` explicitly when `null` is valid:
|
||||
|
||||
```kotlin
|
||||
fun email(user: User?): String? {
|
||||
return user?.email
|
||||
}
|
||||
```
|
||||
|
||||
The compiler rejects `null` in non-nullable arguments, fields, local variables,
|
||||
and return values. Nullable receivers cannot be dereferenced directly. Gotlin
|
||||
smart-casts values after `value != null` branches and guard clauses such as
|
||||
`if (value == null) { return }`. Use `!!` only when an invariant cannot be
|
||||
expressed through control flow:
|
||||
|
||||
```kotlin
|
||||
val required: User = optionalUser!!
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
Gotlin adapts Go `(T, error)` returns into Rust-style `Result<T, Error>` flows.
|
||||
An explicit `Result` return or variable type converts the Go return directly:
|
||||
|
||||
```kotlin
|
||||
fun parse(value: String): Result<Int, Error> {
|
||||
return strconv.atoi(value)
|
||||
}
|
||||
|
||||
fun ping(db: *sql.DB): Result<Unit, Error> {
|
||||
return db.ping()
|
||||
}
|
||||
```
|
||||
|
||||
Use `?` to propagate a Go error or chain a Gotlin function returning `Result`.
|
||||
Explicit panic and fallback operations are available when appropriate:
|
||||
|
||||
```kotlin
|
||||
val required = parse("42").unwrap()
|
||||
val fallback = parse("invalid").unwrapOr(0)
|
||||
```
|
||||
|
||||
Gotlin never inserts implicit panic wrappers. A Go call returning `(T, error)`
|
||||
must have an explicit `Result` context or use `?`, explicit `value, error`
|
||||
destructuring, or `.unwrap()`.
|
||||
|
||||
Gotlin-defined `class` and `data class` values are references automatically,
|
||||
including nested generic types such as `List<User>`. Explicit pointer syntax is
|
||||
reserved for Go interop, for example `*http.Request` and `*pgxpool.Pool`;
|
||||
applying `*` to a Gotlin class is a compile error.
|
||||
|
||||
Enums whose variants carry no payload are represented as string-backed values.
|
||||
The exact variant identifier is used for JSON and PostgreSQL text values:
|
||||
|
||||
```kotlin
|
||||
enum PaymentStatus {
|
||||
PENDING_RESERVATION
|
||||
INITIATED
|
||||
}
|
||||
```
|
||||
|
||||
This reads and writes `"PENDING_RESERVATION"` and `"INITIATED"` directly.
|
||||
Payload-carrying enums remain algebraic sum types.
|
||||
|
||||
## Structural mapping
|
||||
|
||||
Compatible classes and enums can be converted with `mapTo<T>()`:
|
||||
|
||||
```kotlin
|
||||
data class AddressEntity(var city: String)
|
||||
data class AddressResponse(var city: String)
|
||||
data class AccountEntity(var id: String, var address: *AddressEntity)
|
||||
data class AccountResponse(var address: *AddressResponse, var id: String)
|
||||
|
||||
val response = account.mapTo<AccountResponse>()
|
||||
```
|
||||
|
||||
When the surrounding expression provides a target type, the type argument is
|
||||
optional:
|
||||
|
||||
```kotlin
|
||||
fun response(account: AccountEntity): AccountResponse {
|
||||
return account.mapTo()
|
||||
}
|
||||
|
||||
val response: AccountResponse = account.mapTo()
|
||||
val envelope = Envelope(account.mapTo())
|
||||
```
|
||||
|
||||
Use explicit `mapTo<T>()` when assigning to an untyped local or when no target
|
||||
type can be inferred.
|
||||
|
||||
Fields are matched by Gotlin name rather than declaration order. Mapping is
|
||||
recursive across nested classes, pointers, nullable values, lists, mutable
|
||||
lists, maps, and enum payloads. Enum variants are matched by name. Extra source
|
||||
fields and extra target enum variants are allowed; every target field and every
|
||||
source enum variant must be compatible.
|
||||
|
||||
Payloadless enums also map recursively to and from `String`. String-to-enum
|
||||
mapping validates the runtime value and panics for an unknown variant string.
|
||||
|
||||
Incompatible mappings fail compilation with a complete path, for example:
|
||||
|
||||
```text
|
||||
cannot map Account.address.zip: String is incompatible with Int
|
||||
```
|
||||
|
||||
```bash
|
||||
go run ./cmd/gotlinc build ./examples/hello.gt
|
||||
./hello
|
||||
```
|
||||
|
||||
Emit Go source instead:
|
||||
|
||||
```bash
|
||||
go run ./cmd/gotlinc build -src ./examples/hello.gt -o /tmp/hello.go
|
||||
go run /tmp/hello.go
|
||||
```
|
||||
|
||||
Emit or consume package interfaces with repeatable metadata flags:
|
||||
|
||||
```bash
|
||||
gotlinc build -src -metadata-output platform.gti.json \
|
||||
-metadata-package example/platform platform.gt -o platform.go
|
||||
gotlinc build -src -metadata platform.gti.json service.gt -o service.go
|
||||
```
|
||||
|
||||
Run directly:
|
||||
|
||||
```bash
|
||||
go run ./cmd/gotlinc run ./examples/hello.gt
|
||||
```
|
||||
|
||||
## Structured coroutines
|
||||
|
||||
Gotlin coroutines use Go goroutines underneath, but expose only structured
|
||||
scopes. A scope waits for its children, propagates child failures, and cancels
|
||||
sibling coroutine contexts. The removed `worker`, bare `go`, and channel
|
||||
`select` forms are not valid Gotlin syntax; use coroutine scopes, `delay`, and
|
||||
explicit channel `read()`/`send()` operations.
|
||||
|
||||
Coroutine effects are inferred through the call graph. Functions that directly
|
||||
or transitively use coroutine operations receive a hidden scope parameter and
|
||||
can only be called from an ambient coroutine scope. `runBlocking` establishes a
|
||||
scope boundary, so neither a `suspend` modifier nor manually threaded context is
|
||||
needed.
|
||||
|
||||
Use `coroutineContext()` only at Go interop boundaries that require a
|
||||
`context.Context`; it returns the ambient scope context without exposing it in
|
||||
the Gotlin function signature.
|
||||
|
||||
HTTP handlers can establish a request-scoped ambient context with an
|
||||
expression-bodied function:
|
||||
HTTP request contexts can establish an ambient scope with an expression-bodied
|
||||
function:
|
||||
|
||||
```kotlin
|
||||
fun handle(request: *http.Request) = withContext(request.context()) {
|
||||
|
|
@ -314,301 +238,205 @@ fun handle(request: *http.Request) = withContext(request.context()) {
|
|||
}
|
||||
```
|
||||
|
||||
`withContext` is a structured boundary. It waits for children, propagates
|
||||
failures, and, when nested, combines cancellation from the parent coroutine and
|
||||
the supplied Go context.
|
||||
|
||||
For conventional Go APIs whose first parameter is `context.Context`, Gotlin
|
||||
normally injects that ambient context automatically when the argument is
|
||||
omitted:
|
||||
Go calls whose first parameter is `context.Context` receive the ambient context
|
||||
automatically when that argument is omitted:
|
||||
|
||||
```kotlin
|
||||
fun command(): *exec.Cmd {
|
||||
return exec.commandContext("date")
|
||||
}
|
||||
fun command(): *exec.Cmd = exec.commandContext("date")
|
||||
```
|
||||
|
||||
The function becomes contextually effectful and lowers to
|
||||
`exec.CommandContext(gotlinScope.Context(), "date")`. Passing an explicit
|
||||
context remains supported and suppresses injection, which is important for HTTP
|
||||
request contexts and deliberately detached work.
|
||||
Passing an explicit context suppresses automatic injection.
|
||||
|
||||
## Collections and channels
|
||||
|
||||
```kotlin
|
||||
fun load(): Int {
|
||||
delay(10)
|
||||
return 42
|
||||
}
|
||||
val names = listOf<String>("Ada", "Linus")
|
||||
val scores = mapOf<String, Int>("Ada", 10, "Linus", 8)
|
||||
val channel = Channel<String>(1)
|
||||
|
||||
fun main() {
|
||||
runBlocking {
|
||||
val value = async<Int> { return load() }
|
||||
launch { println("loading") }
|
||||
println(value.await())
|
||||
}
|
||||
}
|
||||
channel.send("ready")
|
||||
val message = channel.read()
|
||||
```
|
||||
|
||||
## Type-checked SQL queries
|
||||
Collection types include `List<T>`, `MutableList<T>`, `Map<K, V>`, and
|
||||
`MutableMap<K, V>`.
|
||||
|
||||
Gotlin recognizes a PostgreSQL SQL DSL at compile time. SQL row mappings must be data classes annotated with `@table`. Fields map from lower-camel Gotlin names to `snake_case` columns by default and can override the SQL name with `@column`. Conflict keys use `@id`; database-generated or defaulted fields use `@generated`.
|
||||
## Structural mapping
|
||||
|
||||
`mapTo` maps compatible classes, enums, collections, and nullable values:
|
||||
|
||||
```kotlin
|
||||
import time
|
||||
fun response(account: AccountEntity): AccountResponse = account.mapTo()
|
||||
|
||||
val response = account.mapTo<AccountResponse>()
|
||||
```
|
||||
|
||||
Fields are matched by name and mappings are validated recursively with
|
||||
path-specific compile errors.
|
||||
|
||||
## Typed PostgreSQL DSL
|
||||
|
||||
SQL rows are data classes with table metadata:
|
||||
|
||||
```kotlin
|
||||
@table("accounts")
|
||||
data class AccountRow(
|
||||
@generated @id var id: String,
|
||||
var customerId: String,
|
||||
@column("kind") var accountType: String,
|
||||
var balance: Double,
|
||||
var closedAt: time.Time?
|
||||
)
|
||||
|
||||
fun accountsFor(customerId: String): GotlinSQLQuery {
|
||||
return sql.from<AccountRow>()
|
||||
.where { row -> row.customerId == customerId && row.closedAt == null }
|
||||
.orderByDescending { row -> row.balance }
|
||||
.limit(100)
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
The compiler emits a Go value with this generated support type:
|
||||
|
||||
```go
|
||||
type GotlinSQLQuery struct {
|
||||
SQL string
|
||||
Args []any
|
||||
}
|
||||
```
|
||||
|
||||
The example selects every field, including `@generated` fields, and produces PostgreSQL `$n` placeholders. Arguments are emitted in SQL traversal order. A literal `limit(100)` is embedded after validation; a typed non-literal `Int` limit uses the next placeholder.
|
||||
|
||||
Nullable types use a `?` suffix, currently including forms such as `String?` and `time.Time?`; generated Go fields use pointers. Comparing a nullable field with `null` lowers to `IS NULL` or `IS NOT NULL`. Ordering comparisons support numeric values and `time.Time`; typed `now()` emits `CURRENT_TIMESTAMP` without an argument:
|
||||
### Queries, aliases, joins, groups, and aggregates
|
||||
|
||||
```kotlin
|
||||
sql.from<OutboxRow>()
|
||||
.where { it.publishedAt == null && (it.claimedUntil == null || it.claimedUntil < now()) }
|
||||
.orderBy { it.createdAt }
|
||||
.limit(batchSize)
|
||||
.forUpdate()
|
||||
.skipLocked()
|
||||
.build()
|
||||
```
|
||||
@table("customers")
|
||||
data class CustomerRow(@id var id: String, var name: String)
|
||||
|
||||
The canonical select method order is:
|
||||
data class AccountSummary(
|
||||
var customerId: String,
|
||||
var customerName: String,
|
||||
var total: Double,
|
||||
var entries: Long
|
||||
)
|
||||
|
||||
```text
|
||||
sql.from<Row>()
|
||||
[.select { ... }]
|
||||
[.where { ... }]
|
||||
[.orderBy { ... } | .orderByDescending { ... }]
|
||||
[.limit(Int)]
|
||||
[.forUpdate()]
|
||||
[.skipLocked()]
|
||||
.build() | .fetch(pool, ctx) | .single(pool, ctx) | .iterator(pool, ctx)
|
||||
```
|
||||
|
||||
Each optional method may occur at most once. `select` must be first, `skipLocked` requires `forUpdate`, and clauses are emitted as `WHERE`, `ORDER BY`, `LIMIT`, `FOR UPDATE`, `SKIP LOCKED` in PostgreSQL order.
|
||||
|
||||
### Typed projections
|
||||
|
||||
Projection targets are local data classes. The constructor must contain one direct source-row field per target field, in target declaration order, with exact matching types:
|
||||
|
||||
```kotlin
|
||||
data class AccountSummary(var id: String, var balance: Double)
|
||||
|
||||
fun summaries(pool: *pgxpool.Pool, ctx: context.Context): List<AccountSummary> {
|
||||
return sql.from<AccountRow>()
|
||||
.select { row -> AccountSummary(row.id, row.balance) }
|
||||
.orderBy { it.balance }
|
||||
.fetch(pool, ctx)
|
||||
.unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
The compiler emits `SELECT id, balance`, scans in projection declaration order, and makes `fetch`, `single`, and `iterator` target `AccountSummary` rather than `AccountRow`. The row parameter in later `where` and ordering methods still represents `AccountRow`.
|
||||
|
||||
Select chains can execute directly against a pgx/v5 pool. `fetch(pool, ctx)`
|
||||
returns `Result<List<AccountRow>, Error>` and closes pgx rows after reading and
|
||||
scanning every result in data-class field declaration order:
|
||||
|
||||
```kotlin
|
||||
import context
|
||||
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
fun accountsFor(
|
||||
pool: *pgxpool.Pool,
|
||||
ctx: context.Context,
|
||||
customerId: String
|
||||
): List<AccountRow> {
|
||||
return sql.from<AccountRow>()
|
||||
.where { it.customerId == customerId }
|
||||
.orderBy { it.accountType }
|
||||
.fetch(pool, ctx)
|
||||
.unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
`single(pool, ctx)` returns `Result<AccountRow, Error>` and closes the rows. Use
|
||||
`?` to propagate zero/multiple-row errors or call `.unwrap()` explicitly:
|
||||
|
||||
```kotlin
|
||||
fun account(pool: *pgxpool.Pool, ctx: context.Context, id: String): AccountRow {
|
||||
return sql.from<AccountRow>()
|
||||
.where { it.id == id }
|
||||
.single(pool, ctx)
|
||||
.unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
`iterator(pool, ctx)` returns a `Result` around a streaming pgx iterator. Call
|
||||
`next()` before each `value()`, arrange an explicit `close()`, and inspect
|
||||
`err()` after iteration:
|
||||
|
||||
```kotlin
|
||||
fun printAccounts(pool: *pgxpool.Pool, ctx: context.Context) {
|
||||
val rows = sql.from<AccountRow>().iterator(pool, ctx).unwrap()
|
||||
defer rows.close()
|
||||
|
||||
while (rows.next()) {
|
||||
val account: AccountRow = rows.value()
|
||||
println(account.customerId)
|
||||
fun summaries(minimum: Double): GotlinSQLQuery = sql.from<AccountRow>()
|
||||
.alias("account")
|
||||
.leftJoin<CustomerRow>("customer") { account, customer ->
|
||||
account.customerId == customer.id
|
||||
}
|
||||
|
||||
val checked = rows.err()
|
||||
}
|
||||
```
|
||||
|
||||
Query and scan failures are returned as `Result` errors. `fetch` and `single`
|
||||
close rows internally; an iterator leaves lifecycle control with the caller,
|
||||
and `value()` panics only when called without a successful `next()`. None of the
|
||||
execution terminals require an intermediate `build()`, `query.sql`, or
|
||||
`query.args` access. `build()` remains available for manual execution.
|
||||
|
||||
### Inserts and returning
|
||||
|
||||
Inserts omit every `@generated` field. If all fields are generated, the compiler emits `INSERT ... DEFAULT VALUES`. Conflict handling is optional; conflict fields must have `@id`:
|
||||
|
||||
```kotlin
|
||||
sql.insert<AccountRow>(row)
|
||||
.onConflict { it.id }
|
||||
.doNothing()
|
||||
.select { account, customer ->
|
||||
AccountSummary(
|
||||
account.customerId,
|
||||
customer.name,
|
||||
sum(account.balance),
|
||||
count()
|
||||
)
|
||||
}
|
||||
.where { account, customer -> account.balance > minimum }
|
||||
.groupBy { account, customer -> listOf(account.customerId, customer.name) }
|
||||
.having { account, customer -> sum(account.balance) > minimum }
|
||||
.orderByDescending { account, customer -> sum(account.balance) }
|
||||
.limit(100)
|
||||
.offset(20)
|
||||
.build()
|
||||
```
|
||||
|
||||
Composite conflict keys use `listOf`, for example `.onConflict { listOf(it.tenantId, it.id) }`.
|
||||
Join methods include `join`, `leftJoin`, and `rightJoin`. Aggregates include
|
||||
`count`, `countDistinct`, `sum`, `avg`, `min`, and `max`.
|
||||
|
||||
Updates use a typed lower-camel `set` form because Gotlin does not currently implement Kotlin callable references (`AccountRow::balance`):
|
||||
### Inserts and bulk inserts
|
||||
|
||||
```kotlin
|
||||
sql.insert<AccountRow>(row)
|
||||
val insert = sql.insert<AccountRow>(row).build()
|
||||
val bulk = sql.insertAll<AccountRow>(rows).build()
|
||||
```
|
||||
|
||||
Bulk inserts create runtime-sized PostgreSQL placeholder lists and reject empty
|
||||
input.
|
||||
|
||||
### Conflicts and updates
|
||||
|
||||
```kotlin
|
||||
val upsert = sql.insert<AccountRow>(row)
|
||||
.onConflict { it.id }
|
||||
.doUpdate { excluded -> set(AccountRow.balance, excluded.balance) }
|
||||
.doUpdate { excluded ->
|
||||
AccountRow.balance = excluded.balance
|
||||
}
|
||||
.build()
|
||||
```
|
||||
|
||||
Multiple `set(...)` expressions may appear in one `doUpdate` lambda. Update targets must exist and be `var`; excluded fields must exist and have a compatible type.
|
||||
|
||||
An insert can return the full row or a typed projection. Write chains can use `fetch`, `single`, or `iterator` only after `returning`:
|
||||
|
||||
```kotlin
|
||||
sql.insert<AccountRow>(row)
|
||||
.returning { it }
|
||||
.single(pool, ctx)
|
||||
|
||||
sql.insert<AccountRow>(row)
|
||||
.onConflict { it.id }
|
||||
.doNothing()
|
||||
.returning { value -> AccountSummary(value.id, value.balance) }
|
||||
.single(pool, ctx)
|
||||
```
|
||||
|
||||
The exact insert forms are:
|
||||
|
||||
```text
|
||||
sql.insert<Row>(row)
|
||||
[.onConflict { field | listOf(fields...) }.doNothing() | .doUpdate { ... }]
|
||||
[.returning { it | Projection(it.field, ...) }]
|
||||
.build() | returning execution terminal
|
||||
```
|
||||
|
||||
### Updates and deletes
|
||||
|
||||
Typed updates use one `set` lambda. Targets are mutable source-row fields. Values may be typed names/selectors, literals, `null` for nullable targets, `now()` for timestamps, source-row fields, or numeric `+` and `-` expressions:
|
||||
|
||||
```kotlin
|
||||
sql.update<AccountRow>()
|
||||
val update = sql.update<AccountRow>()
|
||||
.set { row ->
|
||||
set(row.balance, row.balance + amount)
|
||||
set(row.closedAt, now())
|
||||
row.balance = row.balance + amount
|
||||
row.closedAt = now()
|
||||
}
|
||||
.where { it.id == accountId }
|
||||
.returning { row -> AccountSummary(row.id, row.balance) }
|
||||
.single(pool, ctx)
|
||||
.returning { it }
|
||||
.build()
|
||||
```
|
||||
|
||||
Deletes support a typed predicate and the same returning forms:
|
||||
Write targets are checked for existence, mutability, nullability, duplicate
|
||||
assignment, and value compatibility.
|
||||
|
||||
### Execution
|
||||
|
||||
```kotlin
|
||||
sql.delete<AccountRow>()
|
||||
val accounts = sql.from<AccountRow>()
|
||||
.where { it.customerId == customerId }
|
||||
.fetch(pool, ctx)
|
||||
.unwrap()
|
||||
|
||||
val account = sql.from<AccountRow>()
|
||||
.where { it.id == accountId }
|
||||
.returning { it }
|
||||
.single(pool, ctx)
|
||||
.unwrap()
|
||||
```
|
||||
|
||||
The exact write forms are:
|
||||
Execution terminals are `fetch`, `single`, and `iterator`. They return typed
|
||||
`Result` values and use generated row scanners.
|
||||
|
||||
```text
|
||||
sql.update<Row>()
|
||||
.set { row -> set(row.field, value); ... }
|
||||
[.where { predicate }]
|
||||
[.returning { it | Projection(it.field, ...) }]
|
||||
.build() | returning execution terminal
|
||||
## Go interop
|
||||
|
||||
sql.delete<Row>()
|
||||
[.where { predicate }]
|
||||
[.returning { it | Projection(it.field, ...) }]
|
||||
.build() | returning execution terminal
|
||||
Go packages are imported directly:
|
||||
|
||||
```kotlin
|
||||
import http net.http
|
||||
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
|
||||
```
|
||||
|
||||
`where` is intentionally optional for updates and deletes, so omitting it affects the whole table. There is no write `execute` terminal yet; use `build()` for manual `Exec`, or add `returning` and use a query execution terminal.
|
||||
Go selectors are written in lower camel case and emitted with exported Go
|
||||
names. Named arguments construct external Go structs:
|
||||
|
||||
At compile time the DSL checks that the generic row type exists, is a data class with `@table`, has unique mapped columns, and contains every referenced field. It checks known predicate operand types, nullable operations, insert row types, `@id` conflict metadata, update mutability and value compatibility, method cardinality/order, lock dependencies, and projection field counts/types.
|
||||
```kotlin
|
||||
val client = http.Client(timeout = 3 * time.second)
|
||||
```
|
||||
|
||||
Current SQL limitations:
|
||||
`go/types` and `go/packages` provide function, method, field, alias, tuple,
|
||||
variadic, and error-return signatures to semantic analysis and the LSP.
|
||||
|
||||
- Predicates support `&&`, `||`, `!`, `==`, `!=`, and numeric/timestamp `<`, `<=`, `>`, `>=`. Values must have a compiler-known type; external values become `$n` arguments.
|
||||
- Projections and `returning` do not support scalar results, aliases, computed expressions, aggregates, external structs, or reordered/coerced target types. They accept a full row or direct fields passed to a local data-class constructor.
|
||||
- Standalone update values do not support SQL functions other than `now()`, string expressions, intervals, casts, subqueries, or arbitrary SQL fragments. Upsert `doUpdate` values still come only from the `excluded` row.
|
||||
- Inserts emit one row at a time. Bulk/multi-row inserts are not implemented.
|
||||
- Table and column annotations are validated unquoted SQL identifiers. Joins, aliases, grouping, aggregates, `OFFSET`, lock strengths other than `FOR UPDATE`, and conflict predicates are not implemented.
|
||||
- The compiler does not perform migrations, schema generation, database connections, or schema introspection.
|
||||
## Build and run
|
||||
|
||||
Language server:
|
||||
```bash
|
||||
go run ./cmd/gotlinc build ./examples/hello.gt
|
||||
./hello
|
||||
|
||||
go run ./cmd/gotlinc run ./examples/hello.gt
|
||||
```
|
||||
|
||||
Emit Go source:
|
||||
|
||||
```bash
|
||||
go run ./cmd/gotlinc build -src ./examples/hello.gt -o /tmp/hello.go
|
||||
```
|
||||
|
||||
Emit and consume package interfaces:
|
||||
|
||||
```bash
|
||||
gotlinc build -src \
|
||||
-metadata-output platform.gti.json \
|
||||
-metadata-package example/platform \
|
||||
platform.gt -o platform.go
|
||||
|
||||
gotlinc build -src \
|
||||
-metadata platform.gti.json \
|
||||
service.gt -o service.go
|
||||
```
|
||||
|
||||
## Language server and editor
|
||||
|
||||
Build the language server:
|
||||
|
||||
```bash
|
||||
go build -o ./bin/gotlin-lsp ./cmd/gotlin-lsp
|
||||
./bin/gotlin-lsp
|
||||
```
|
||||
|
||||
VS Code extension:
|
||||
The VS Code extension under `tools/vscode-gotlin` provides syntax highlighting,
|
||||
snippets, diagnostics, hover, document symbols, definitions, references, and a
|
||||
`gopls` bridge for imported Go APIs.
|
||||
|
||||
```bash
|
||||
cd ./tools/vscode-gotlin
|
||||
cd tools/vscode-gotlin
|
||||
npm install
|
||||
npm run build
|
||||
npm run check
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `val` is immutable after initialization; `var` can be reassigned.
|
||||
- Type inference is local to declarations without an explicit type.
|
||||
- Top-level declarations currently support functions, classes, and interfaces.
|
||||
- `gotlinc build` produces an executable by default. If `-o` is omitted, the output name is derived from the input file name.
|
||||
- `gotlinc build -src` emits Go source instead of a binary.
|
||||
- `gotlinc` supports `build` and `run`, and defaults to `build` if no subcommand is given.
|
||||
- Gotlin source files use the `.gt` extension.
|
||||
- `gotlin-lsp` provides diagnostics, hover, and go-to-definition over stdio.
|
||||
- `gotlin-lsp` can optionally use `gopls` for hover and definition on Go-imported symbols.
|
||||
- the VS Code extension adds syntax highlighting, snippets, and launches the LSP for `.gt` files.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue