diff --git a/examples/kotlin/build.gradle b/examples/kotlin/build.gradle index 7f4477e225..fd331077ae 100644 --- a/examples/kotlin/build.gradle +++ b/examples/kotlin/build.gradle @@ -27,4 +27,7 @@ compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } -buildScan { termsOfServiceUrl = "https://gradle.com/terms-of-service"; termsOfServiceAgree = "yes" } \ No newline at end of file +buildScan { + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" +} diff --git a/examples/kotlin/gradle/wrapper/gradle-wrapper.properties b/examples/kotlin/gradle/wrapper/gradle-wrapper.properties index 3cdb37dd29..b5354905d6 100644 --- a/examples/kotlin/gradle/wrapper/gradle-wrapper.properties +++ b/examples/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Sat Jan 25 10:45:34 EST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/examples/kotlin/settings.gradle b/examples/kotlin/settings.gradle index d51bf78f7f..5a094655c6 100644 --- a/examples/kotlin/settings.gradle +++ b/examples/kotlin/settings.gradle @@ -1,2 +1,5 @@ -rootProject.name = 'dbtest' +plugins { + id("com.gradle.enterprise").version("3.1.1") +} +rootProject.name = 'dbtest' diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt index 46b71b248a..066918ee6b 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt @@ -6,7 +6,12 @@ import java.time.OffsetDateTime enum class BookType(val value: String) { FICTION("FICTION"), - NONFICTION("NONFICTION") + NONFICTION("NONFICTION"); + + companion object { + private val map = BookType.values().associateBy(BookType::value) + fun lookup(value: String) = map[value] + } } data class Author ( @@ -22,6 +27,6 @@ data class Book ( val title: String, val year: Int, val available: OffsetDateTime, - val tags: Array + val tags: List ) diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index e7d6d4764c..e81fc242e1 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -4,6 +4,7 @@ package com.example.booktest.postgresql import java.sql.Connection import java.sql.SQLException +import java.sql.Types import java.time.OffsetDateTime const val booksByTags = """-- name: booksByTags :many @@ -23,7 +24,7 @@ data class BooksByTagsRow ( val title: String, val name: String, val isbn: String, - val tags: Array + val tags: List ) const val booksByTitleYear = """-- name: booksByTitleYear :many @@ -69,7 +70,7 @@ data class CreateBookParams ( val title: String, val year: Int, val available: OffsetDateTime, - val tags: Array + val tags: List ) const val deleteBook = """-- name: deleteBook :exec @@ -95,7 +96,7 @@ WHERE book_id = ? data class UpdateBookParams ( val title: String, - val tags: Array, + val tags: List, val bookId: Int ) @@ -107,7 +108,7 @@ WHERE book_id = ? data class UpdateBookISBNParams ( val title: String, - val tags: Array, + val tags: List, val bookId: Int, val isbn: String ) @@ -115,9 +116,9 @@ data class UpdateBookISBNParams ( class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) - fun booksByTags(dollar_1: Array): List { + fun booksByTags(dollar_1: List): List { val stmt = conn.prepareStatement(booksByTags) - stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1)) + stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1.toTypedArray())) return stmt.executeQuery().use { results -> val ret = mutableListOf() @@ -127,7 +128,7 @@ class QueriesImpl(private val conn: Connection) { results.getString(2), results.getString(3), results.getString(4), - results.getArray(5).array as Array + (results.getArray(5).array as Array).toList() )) } ret @@ -147,11 +148,11 @@ class QueriesImpl(private val conn: Connection) { results.getInt(1), results.getInt(2), results.getString(3), - BookType.valueOf(results.getString(4)), + BookType.lookup(results.getString(4))!!, results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - results.getArray(8).array as Array + (results.getArray(8).array as Array).toList() )) } ret @@ -183,11 +184,11 @@ class QueriesImpl(private val conn: Connection) { val stmt = conn.prepareStatement(createBook) stmt.setInt(1, arg.authorId) stmt.setString(2, arg.isbn) - stmt.setString(3, arg.booktype.value) + stmt.setObject(3, arg.booktype.value, Types.OTHER) stmt.setString(4, arg.title) stmt.setInt(5, arg.year) stmt.setObject(6, arg.available) - stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) return stmt.executeQuery().use { results -> if (!results.next()) { @@ -197,11 +198,11 @@ class QueriesImpl(private val conn: Connection) { results.getInt(1), results.getInt(2), results.getString(3), - BookType.valueOf(results.getString(4)), + BookType.lookup(results.getString(4))!!, results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - results.getArray(8).array as Array + (results.getArray(8).array as Array).toList() ) if (results.next()) { throw SQLException("expected one row in result set, but got many") @@ -252,11 +253,11 @@ class QueriesImpl(private val conn: Connection) { results.getInt(1), results.getInt(2), results.getString(3), - BookType.valueOf(results.getString(4)), + BookType.lookup(results.getString(4))!!, results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - results.getArray(8).array as Array + (results.getArray(8).array as Array).toList() ) if (results.next()) { throw SQLException("expected one row in result set, but got many") @@ -269,7 +270,7 @@ class QueriesImpl(private val conn: Connection) { fun updateBook(arg: UpdateBookParams) { val stmt = conn.prepareStatement(updateBook) stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) stmt.setInt(3, arg.bookId) stmt.execute() @@ -280,7 +281,7 @@ class QueriesImpl(private val conn: Connection) { fun updateBookISBN(arg: UpdateBookISBNParams) { val stmt = conn.prepareStatement(updateBookISBN) stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) stmt.setInt(3, arg.bookId) stmt.setString(4, arg.isbn) diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt index 6b7f38d395..e4dd8e7db7 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt @@ -7,7 +7,12 @@ import java.time.LocalDateTime // Venues can be either open or closed enum class Status(val value: String) { OPEN("op!en"), - CLOSED("clo@sed") + CLOSED("clo@sed"); + + companion object { + private val map = Status.values().associateBy(Status::value) + fun lookup(value: String) = map[value] + } } data class City ( @@ -19,14 +24,14 @@ data class City ( data class Venue ( val id: Int, val status: Status, - val statuses: Array, + val statuses: List, // This value appears in public URLs val slug: String, val name: String, val city: String, val spotifyPlaylist: String, val songkickId: String?, - val tags: Array, + val tags: List, val createdAt: LocalDateTime ) diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt index fa540fc82e..c67fd0b83f 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt @@ -4,6 +4,7 @@ package com.example.ondeck import java.sql.Connection import java.sql.SQLException +import java.sql.Types import java.time.LocalDateTime interface Queries { diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt index 4da97fec32..60777d19c9 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt @@ -4,6 +4,7 @@ package com.example.ondeck import java.sql.Connection import java.sql.SQLException +import java.sql.Types import java.time.LocalDateTime const val createCity = """-- name: createCity :one @@ -49,8 +50,8 @@ data class CreateVenueParams ( val city: String, val spotifyPlaylist: String, val status: Status, - val statuses: Array, - val tags: Array + val statuses: List, + val tags: List ) const val deleteVenue = """-- name: deleteVenue :exec @@ -159,9 +160,9 @@ class QueriesImpl(private val conn: Connection) : Queries { stmt.setString(2, arg.name) stmt.setString(3, arg.city) stmt.setString(4, arg.spotifyPlaylist) - stmt.setString(5, arg.status.value) + stmt.setObject(5, arg.status.value, Types.OTHER) stmt.setArray(6, conn.createArrayOf("status", arg.statuses.map { v -> v.value }.toTypedArray())) - stmt.setArray(7, conn.createArrayOf("text", arg.tags)) + stmt.setArray(7, conn.createArrayOf("text", arg.tags.toTypedArray())) return stmt.executeQuery().use { results -> if (!results.next()) { @@ -216,14 +217,14 @@ class QueriesImpl(private val conn: Connection) : Queries { } val ret = Venue( results.getInt(1), - Status.valueOf(results.getString(2)), - (results.getArray(3).array as Array).map { v -> Status.valueOf(v) }.toTypedArray(), + Status.lookup(results.getString(2))!!, + (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), results.getString(4), results.getString(5), results.getString(6), results.getString(7), results.getString(8), - results.getArray(9).array as Array, + (results.getArray(9).array as Array).toList(), results.getObject(10, LocalDateTime::class.java) ) if (results.next()) { @@ -259,14 +260,14 @@ class QueriesImpl(private val conn: Connection) : Queries { while (results.next()) { ret.add(Venue( results.getInt(1), - Status.valueOf(results.getString(2)), - (results.getArray(3).array as Array).map { v -> Status.valueOf(v) }.toTypedArray(), + Status.lookup(results.getString(2))!!, + (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), results.getString(4), results.getString(5), results.getString(6), results.getString(7), results.getString(8), - results.getArray(9).array as Array, + (results.getArray(9).array as Array).toList(), results.getObject(10, LocalDateTime::class.java) )) } diff --git a/examples/kotlin/src/main/resources/query.sql b/examples/kotlin/src/main/resources/authors/query.sql similarity index 100% rename from examples/kotlin/src/main/resources/query.sql rename to examples/kotlin/src/main/resources/authors/query.sql diff --git a/examples/kotlin/src/main/resources/schema.sql b/examples/kotlin/src/main/resources/authors/schema.sql similarity index 100% rename from examples/kotlin/src/main/resources/schema.sql rename to examples/kotlin/src/main/resources/authors/schema.sql diff --git a/examples/kotlin/src/main/resources/booktest/postgresql/query.sql b/examples/kotlin/src/main/resources/booktest/postgresql/query.sql new file mode 100644 index 0000000000..f4537c603e --- /dev/null +++ b/examples/kotlin/src/main/resources/booktest/postgresql/query.sql @@ -0,0 +1,60 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE author_id = $1; + +-- name: GetBook :one +SELECT * FROM books +WHERE book_id = $1; + +-- name: DeleteBook :exec +DELETE FROM books +WHERE book_id = $1; + +-- name: BooksByTitleYear :many +SELECT * FROM books +WHERE title = $1 AND year = $2; + +-- name: BooksByTags :many +SELECT + book_id, + title, + name, + isbn, + tags +FROM books +LEFT JOIN authors ON books.author_id = authors.author_id +WHERE tags && $1::varchar[]; + +-- name: CreateAuthor :one +INSERT INTO authors (name) VALUES ($1) +RETURNING *; + +-- name: CreateBook :one +INSERT INTO books ( + author_id, + isbn, + booktype, + title, + year, + available, + tags +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING *; + +-- name: UpdateBook :exec +UPDATE books +SET title = $1, tags = $2 +WHERE book_id = $3; + +-- name: UpdateBookISBN :exec +UPDATE books +SET title = $1, tags = $2, isbn = $4 +WHERE book_id = $3; diff --git a/examples/kotlin/src/main/resources/booktest/postgresql/schema.sql b/examples/kotlin/src/main/resources/booktest/postgresql/schema.sql new file mode 100644 index 0000000000..0816931a81 --- /dev/null +++ b/examples/kotlin/src/main/resources/booktest/postgresql/schema.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS books CASCADE; +DROP TYPE IF EXISTS book_type CASCADE; +DROP TABLE IF EXISTS authors CASCADE; +DROP FUNCTION IF EXISTS say_hello(text) CASCADE; + +CREATE TABLE authors ( + author_id SERIAL PRIMARY KEY, + name text NOT NULL DEFAULT '' +); + +CREATE INDEX authors_name_idx ON authors(name); + +CREATE TYPE book_type AS ENUM ( + 'FICTION', + 'NONFICTION' +); + +CREATE TABLE books ( + book_id SERIAL PRIMARY KEY, + author_id integer NOT NULL REFERENCES authors(author_id), + isbn text NOT NULL DEFAULT '' UNIQUE, + booktype book_type NOT NULL DEFAULT 'FICTION', + title text NOT NULL DEFAULT '', + year integer NOT NULL DEFAULT 2000, + available timestamp with time zone NOT NULL DEFAULT 'NOW()', + tags varchar[] NOT NULL DEFAULT '{}' +); + +CREATE INDEX books_title_idx ON books(title, year); + +CREATE FUNCTION say_hello(text) RETURNS text AS $$ +BEGIN + RETURN CONCAT('hello ', $1); +END; +$$ LANGUAGE plpgsql; + +CREATE INDEX books_title_lower_idx ON books(title); diff --git a/examples/kotlin/src/main/resources/ondeck/query/city.sql b/examples/kotlin/src/main/resources/ondeck/query/city.sql new file mode 100644 index 0000000000..f34dc9961e --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/query/city.sql @@ -0,0 +1,26 @@ +-- name: ListCities :many +SELECT * +FROM city +ORDER BY name; + +-- name: GetCity :one +SELECT * +FROM city +WHERE slug = $1; + +-- name: CreateCity :one +-- Create a new city. The slug must be unique. +-- This is the second line of the comment +-- This is the third line +INSERT INTO city ( + name, + slug +) VALUES ( + $1, + $2 +) RETURNING *; + +-- name: UpdateCityName :exec +UPDATE city +SET name = $2 +WHERE slug = $1; diff --git a/examples/kotlin/src/main/resources/ondeck/query/venue.sql b/examples/kotlin/src/main/resources/ondeck/query/venue.sql new file mode 100644 index 0000000000..8c6bd02664 --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/query/venue.sql @@ -0,0 +1,49 @@ +-- name: ListVenues :many +SELECT * +FROM venue +WHERE city = $1 +ORDER BY name; + +-- name: DeleteVenue :exec +DELETE FROM venue +WHERE slug = $1 AND slug = $1; + +-- name: GetVenue :one +SELECT * +FROM venue +WHERE slug = $1 AND city = $2; + +-- name: CreateVenue :one +INSERT INTO venue ( + slug, + name, + city, + created_at, + spotify_playlist, + status, + statuses, + tags +) VALUES ( + $1, + $2, + $3, + NOW(), + $4, + $5, + $6, + $7 +) RETURNING id; + +-- name: UpdateVenueName :one +UPDATE venue +SET name = $2 +WHERE slug = $1 +RETURNING id; + +-- name: VenueCountByCity :many +SELECT + city, + count(*) +FROM venue +GROUP BY 1 +ORDER BY 1; diff --git a/examples/kotlin/src/main/resources/ondeck/schema/0001_city.sql b/examples/kotlin/src/main/resources/ondeck/schema/0001_city.sql new file mode 100644 index 0000000000..af38f16bb5 --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/schema/0001_city.sql @@ -0,0 +1,4 @@ +CREATE TABLE city ( + slug text PRIMARY KEY, + name text NOT NULL +) diff --git a/examples/kotlin/src/main/resources/ondeck/schema/0002_venue.sql b/examples/kotlin/src/main/resources/ondeck/schema/0002_venue.sql new file mode 100644 index 0000000000..940de7a5a8 --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/schema/0002_venue.sql @@ -0,0 +1,18 @@ +CREATE TYPE status AS ENUM ('op!en', 'clo@sed'); +COMMENT ON TYPE status IS 'Venues can be either open or closed'; + +CREATE TABLE venues ( + id SERIAL primary key, + dropped text, + status status not null, + statuses status[], + slug text not null, + name varchar(255) not null, + city text not null references city(slug), + spotify_playlist varchar not null, + songkick_id text, + tags text[] +); +COMMENT ON TABLE venues IS 'Venues are places where muisc happens'; +COMMENT ON COLUMN venues.slug IS 'This value appears in public URLs'; + diff --git a/examples/kotlin/src/main/resources/ondeck/schema/0003_add_column.sql b/examples/kotlin/src/main/resources/ondeck/schema/0003_add_column.sql new file mode 100644 index 0000000000..9b334bccce --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/schema/0003_add_column.sql @@ -0,0 +1,3 @@ +ALTER TABLE venues RENAME TO venue; +ALTER TABLE venue ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT NOW(); +ALTER TABLE venue DROP COLUMN dropped; diff --git a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt index fb0ce85904..0a42a0dcc7 100644 --- a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt @@ -1,46 +1,20 @@ package com.example.authors -import org.junit.jupiter.api.AfterEach +import com.example.dbtest.DbTestExtension import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.nio.file.Files -import java.nio.file.Paths +import org.junit.jupiter.api.extension.RegisterExtension import java.sql.Connection -import java.sql.DriverManager -const val schema = "dinosql_test" +class QueriesImplTest() { -class QueriesImplTest { - lateinit var schemaConn: Connection - lateinit var conn: Connection - - @BeforeEach - fun setup() { - val user = System.getenv("PG_USER") ?: "postgres" - val pass = System.getenv("PG_PASSWORD") ?: "mysecretpassword" - val host = System.getenv("PG_HOST") ?: "127.0.0.1" - val port = System.getenv("PG_PORT") ?: "5432" - val db = System.getenv("PG_DATABASE") ?: "dinotest" - val url = "jdbc:postgresql://$host:$port/$db?user=$user&password=$pass&sslmode=disable" - println("db: $url") - - schemaConn = DriverManager.getConnection(url) - schemaConn.createStatement().execute("CREATE SCHEMA $schema") - - conn = DriverManager.getConnection("$url¤tSchema=$schema") - val stmt = Files.readString(Paths.get("src/main/resources/schema.sql")) - conn.createStatement().execute(stmt) - } - - @AfterEach - fun teardown() { - schemaConn.createStatement().execute("DROP SCHEMA $schema CASCADE") + companion object { + @JvmField @RegisterExtension val dbtest = DbTestExtension("src/main/resources/authors/schema.sql") } @Test fun testCreateAuthor() { - val db = QueriesImpl(conn) + val db = QueriesImpl(dbtest.getConnection()) val initialAuthors = db.listAuthors() assert(initialAuthors.isEmpty()) @@ -63,7 +37,7 @@ class QueriesImplTest { @Test fun testNull() { - val db = QueriesImpl(conn) + val db = QueriesImpl(dbtest.getConnection()) val initialAuthors = db.listAuthors() assert(initialAuthors.isEmpty()) diff --git a/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt new file mode 100644 index 0000000000..fb0e48e3d4 --- /dev/null +++ b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt @@ -0,0 +1,109 @@ +package com.example.booktest.postgresql + +import com.example.dbtest.DbTestExtension +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +class QueriesImplTest { + companion object { + @JvmField @RegisterExtension val dbtest = DbTestExtension("src/main/resources/booktest/postgresql/schema.sql") + } + + @Test + fun testQueries() { + val conn = dbtest.getConnection() + val db = QueriesImpl(conn) + val author = db.createAuthor("Unknown Master") + + // Start a transaction + conn.autoCommit = false + db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "1", + title = "my book title", + booktype = BookType.NONFICTION, + year = 2016, + available = OffsetDateTime.now(), + tags = listOf() + ) + ) + + val b1 = db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "2", + title = "the second book", + booktype = BookType.NONFICTION, + year = 2016, + available = OffsetDateTime.now(), + tags = listOf("cool", "unique") + ) + ) + + db.updateBook( + UpdateBookParams( + bookId = b1.bookId, + title = "changed second title", + tags = listOf("cool", "disastor") + ) + ) + + val b3 = db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "3", + title = "the third book", + booktype = BookType.NONFICTION, + year = 2001, + available = OffsetDateTime.now(), + tags = listOf("cool") + ) + ) + + db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "4", + title = "4th place finisher", + booktype = BookType.NONFICTION, + year = 2011, + available = OffsetDateTime.now(), + tags = listOf("other") + ) + ) + + // Commit transaction + conn.commit() + conn.autoCommit = true + + // ISBN update fails because parameters are not in sequential order. After changing $N to ?, ordering is lost, + // and the parameters are filled into the wrong slots. +// db.updateBookISBN( +// UpdateBookISBNParams( +// bookId = b3.bookId, +// isbn = "NEW ISBN", +// title = "never ever gonna finish, a quatrain", +// tags = listOf("someother") +// ) +// ) + + val books0 = db.booksByTitleYear(BooksByTitleYearParams("my book title", 2016)) + + val formatter = DateTimeFormatter.ISO_DATE_TIME + for (book in books0) { + println("Book ${book.bookId} (${book.booktype}): ${book.title} available: ${book.available.format(formatter)}") + val author = db.getAuthor(book.authorId) + println("Book ${book.bookId} author: ${author.name}") + } + + // find a book with either "cool" or "other" tag + println("---------\\nTag search results:\\n") + val res = db.booksByTags(listOf("cool", "other", "someother")) + for (ab in res) { + println("Book ${ab.bookId}: '${ab.title}', Author: '${ab.name}', ISBN: '${ab.isbn}' Tags: '${ab.tags.toList()}'") + } + } +} \ No newline at end of file diff --git a/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt b/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt new file mode 100644 index 0000000000..66f831477e --- /dev/null +++ b/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt @@ -0,0 +1,49 @@ +package com.example.dbtest + +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import java.nio.file.Files +import java.nio.file.Paths +import java.sql.Connection +import java.sql.DriverManager +import kotlin.streams.toList + +const val schema = "dinosql_test" + +class DbTestExtension(private val migrationsPath: String) : BeforeEachCallback, AfterEachCallback { + private val schemaConn: Connection + private val url: String + + init { + val user = System.getenv("PG_USER") ?: "postgres" + val pass = System.getenv("PG_PASSWORD") ?: "mysecretpassword" + val host = System.getenv("PG_HOST") ?: "127.0.0.1" + val port = System.getenv("PG_PORT") ?: "5432" + val db = System.getenv("PG_DATABASE") ?: "dinotest" + url = "jdbc:postgresql://$host:$port/$db?user=$user&password=$pass&sslmode=disable" + + schemaConn = DriverManager.getConnection(url) + } + + override fun beforeEach(context: ExtensionContext) { + schemaConn.createStatement().execute("CREATE SCHEMA $schema") + val path = Paths.get(migrationsPath) + val migrations = if (Files.isDirectory(path)) { + Files.list(path).filter{ it.toString().endsWith(".sql")}.sorted().map { Files.readString(it) }.toList() + } else { + listOf(Files.readString(path)) + } + migrations.forEach { + getConnection().createStatement().execute(it) + } + } + + override fun afterEach(context: ExtensionContext) { + schemaConn.createStatement().execute("DROP SCHEMA $schema CASCADE") + } + + fun getConnection(): Connection { + return DriverManager.getConnection("$url¤tSchema=$schema") + } +} \ No newline at end of file diff --git a/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt new file mode 100644 index 0000000000..8a2ff68dce --- /dev/null +++ b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt @@ -0,0 +1,51 @@ +package com.example.ondeck + +import com.example.dbtest.DbTestExtension +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class QueriesImplTest { + companion object { + @JvmField @RegisterExtension val dbtest = DbTestExtension("src/main/resources/ondeck/schema") + } + + @Test + fun testQueries() { + val q = QueriesImpl(dbtest.getConnection()) + val city = q.createCity( + CreateCityParams( + slug = "san-francisco", + name = "San Francisco" + ) + ) + val venueId = q.createVenue( + CreateVenueParams( + slug = "the-fillmore", + name = "The Fillmore", + city = city.slug, + spotifyPlaylist = "spotify=uri", + status = Status.OPEN, + statuses = listOf(Status.OPEN, Status.CLOSED), + tags = listOf("rock", "punk") + ) + ) + val venue = q.getVenue( + GetVenueParams( + slug = "the-fillmore", + city = city.slug + ) + ) + assertEquals(venueId, venue.id) + + assertEquals(city, q.getCity(city.slug)) + assertEquals(listOf(VenueCountByCityRow(city.slug, 1)), q.venueCountByCity()) + assertEquals(listOf(city), q.listCities()) + assertEquals(listOf(venue), q.listVenues(city.slug)) + + // These updates fail because parameters are not in sequential order. After changing $N to ?, ordering is lost, + // and the parameters are filled into the wrong slots. +// q.updateCityName(UpdateCityNameParams(slug = city.slug, name = "SF")) +// q.updateVenueName(UpdateVenueNameParams(slug = venue.slug, name = "Fillmore")) + } +} \ No newline at end of file diff --git a/examples/sqlc.json b/examples/sqlc.json index 98f3d5fdac..f892a0bb11 100644 --- a/examples/sqlc.json +++ b/examples/sqlc.json @@ -39,8 +39,8 @@ { "name": "com.example.authors", "path": "kotlin/src/main/kotlin/com/example/authors", - "schema": "kotlin/src/main/resources/schema.sql", - "queries": "kotlin/src/main/resources/query.sql", + "schema": "kotlin/src/main/resources/authors/schema.sql", + "queries": "kotlin/src/main/resources/authors/query.sql", "engine": "postgresql", "language": "kotlin" }, @@ -64,8 +64,8 @@ { "name": "com.example.booktest.postgresql", "path": "kotlin/src/main/kotlin/com/example/booktest/postgresql", - "schema": "booktest/postgresql/schema.sql", - "queries": "booktest/postgresql/query.sql", + "schema": "kotlin/src/main/resources/booktest/postgresql/schema.sql", + "queries": "kotlin/src/main/resources/booktest/postgresql/query.sql", "engine": "postgresql", "language": "kotlin" } diff --git a/internal/dinosql/kotlin/gen.go b/internal/dinosql/kotlin/gen.go index c7947bf71d..a6029ecd44 100644 --- a/internal/dinosql/kotlin/gen.go +++ b/internal/dinosql/kotlin/gen.go @@ -79,28 +79,60 @@ func (v KtQueryValue) Type() string { panic("no type for KtQueryValue: " + v.Name) } +func jdbcSet(t ktType, idx int, name string) string { + if t.IsEnum && t.IsArray { + return fmt.Sprintf(`stmt.setArray(%d, conn.createArrayOf("%s", %s.map { v -> v.value }.toTypedArray()))`, idx, t.DataType, name) + } + if t.IsEnum { + return fmt.Sprintf("stmt.setObject(%d, %s.value, %s)", idx, name, "Types.OTHER") + } + if t.IsArray { + return fmt.Sprintf(`stmt.setArray(%d, conn.createArrayOf("%s", %s.toTypedArray()))`, idx, t.DataType, name) + } + if t.IsTime() { + return fmt.Sprintf("stmt.setObject(%d, %s)", idx, name) + } + return fmt.Sprintf("stmt.set%s(%d, %s)", t.Name, idx, name) +} + func (v KtQueryValue) Params() string { if v.isEmpty() { return "" } var out []string if v.Struct == nil { - out = append(out, fmt.Sprintf("stmt.%s(%d, %s)", v.Typ.jdbcSetter(), 1, v.Typ.jdbcValue(v.Name))) + out = append(out, jdbcSet(v.Typ, 1, v.Name)) } else { for i, f := range v.Struct.Fields { - out = append(out, fmt.Sprintf("stmt.%s(%d, %s)", f.Type.jdbcSetter(), i+1, f.Type.jdbcValue(v.Name+"."+f.Name))) + out = append(out, jdbcSet(f.Type, i+1, v.Name+"."+f.Name)) } } return strings.Join(out, "\n ") } +func jdbcGet(t ktType, idx int) string { + if t.IsEnum && t.IsArray { + return fmt.Sprintf(`(results.getArray(%d).array as Array).map { v -> %s.lookup(v)!! }.toList()`, idx, t.Name) + } + if t.IsEnum { + return fmt.Sprintf("%s.lookup(results.getString(%d))!!", t.Name, idx) + } + if t.IsArray { + return fmt.Sprintf(`(results.getArray(%d).array as Array<%s>).toList()`, idx, t.Name) + } + if t.IsTime() { + return fmt.Sprintf(`results.getObject(%d, %s::class.java)`, idx, t.Name) + } + return fmt.Sprintf(`results.get%s(%d)`, t.Name, idx) +} + func (v KtQueryValue) ResultSet() string { var out []string if v.Struct == nil { - out = append(out, v.Typ.fromJDBCValue(fmt.Sprintf("%s.%s(%d)", v.Name, v.Typ.jdbcGetter(), 1))) + out = append(out, jdbcGet(v.Typ, 1)) } else { for i, f := range v.Struct.Fields { - out = append(out, f.Type.fromJDBCValue(fmt.Sprintf("%s.%s(%d)", v.Name, f.Type.jdbcGetter(), i+1))) + out = append(out, jdbcGet(f.Type, i+1)) } } ret := strings.Join(out, ",\n ") @@ -110,19 +142,6 @@ func (v KtQueryValue) ResultSet() string { return ret } -type KtQueryParam struct { - Name string - Typ string -} - -func (p KtQueryParam) Getter() string { - return "get" + strings.TrimSuffix(p.Typ, "?") -} - -func (p KtQueryParam) Setter() string { - return "set" + strings.TrimSuffix(p.Typ, "?") -} - // A struct used to generate methods and fields on the Queries struct type KtQuery struct { ClassName string @@ -279,6 +298,25 @@ func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filenam return false } + hasEnum := func() bool { + for _, q := range gq { + if !q.Arg.isEmpty() { + if q.Arg.IsStruct() { + for _, f := range q.Arg.Struct.Fields { + if f.Type.IsEnum { + return true + } + } + } else { + if q.Arg.Typ.IsEnum { + return true + } + } + } + } + return false + } + std := map[string]struct{}{ "java.sql.Connection": {}, "java.sql.SQLException": {}, @@ -295,6 +333,9 @@ func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filenam if uses("OffsetDateTime") { std["java.time.OffsetDateTime"] = struct{}{} } + if hasEnum() { + std["java.sql.Types"] = struct{}{} + } pkg := make(map[string]struct{}) @@ -422,7 +463,7 @@ type ktType struct { func (t ktType) String() string { v := t.Name if t.IsArray { - v = fmt.Sprintf("Array<%s>", v) + v = fmt.Sprintf("List<%s>", v) } else if t.IsNull { v += "?" } @@ -433,18 +474,11 @@ func (t ktType) jdbcSetter() string { return "set" + t.jdbcType() } -func (t ktType) jdbcGetter() string { - return "get" + t.jdbcType() -} - func (t ktType) jdbcType() string { if t.IsArray { return "Array" } - if t.IsEnum { - return "String" - } - if t.IsTime() { + if t.IsEnum || t.IsTime() { return "Object" } return t.Name @@ -454,36 +488,6 @@ func (t ktType) IsTime() bool { return t.Name == "LocalDate" || t.Name == "LocalDateTime" || t.Name == "LocalTime" || t.Name == "OffsetDateTime" } -func (t ktType) jdbcValue(name string) string { - if t.IsEnum && t.IsArray { - return fmt.Sprintf(`conn.createArrayOf("%s", %s.map { v -> v.value }.toTypedArray())`, t.DataType, name) - } - if t.IsEnum { - return name + ".value" - } - if t.IsArray { - return fmt.Sprintf(`conn.createArrayOf("%s", %s)`, t.DataType, name) - } - return name -} - -func (t ktType) fromJDBCValue(expr string) string { - if t.IsEnum && t.IsArray { - return fmt.Sprintf(`(%s.array as Array).map { v -> %s.valueOf(v) }.toTypedArray()`, expr, t.Name) - } - if t.IsEnum { - return fmt.Sprintf("%s.valueOf(%s)", t.Name, expr) - } - if t.IsArray { - return fmt.Sprintf(`%s.array as Array<%s>`, expr, t.Name) - } - if t.IsTime() { - expr = strings.TrimSuffix(expr, ")") - return fmt.Sprintf(`%s, %s::class.java)`, expr, t.Name) - } - return expr -} - func (r Result) ktType(col core.Column, settings dinosql.CombinedSettings) ktType { typ, isEnum := r.ktInnerType(col, settings) return ktType{ @@ -777,7 +781,12 @@ enum class {{.Name}}(val value: String) { {{- range $i, $e := .Constants}} {{- if $i }},{{end}} {{.Name}}("{{.Value}}") - {{- end}} + {{- end}}; + + companion object { + private val map = {{.Name}}.values().associateBy({{.Name}}::value) + fun lookup(value: String) = map[value] + } } {{end}}