From 61b5381c73cea1c9ac6646452b8bdd3ee6495f3f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 28 Mar 2025 22:16:37 -0400 Subject: [PATCH] WIP on Groovy Json ITs --- .../tests/integration/JsonDocument.groovy | 15 + .../tests/integration/JsonFunctions.groovy | 382 +++++++++++++++++- 2 files changed, 394 insertions(+), 3 deletions(-) diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy index 71594e8..db6c1bb 100644 --- a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy @@ -25,4 +25,19 @@ class JsonDocument { static void load(ThrowawayDatabase db, String tableName = TEST_TABLE) { testDocuments.forEach { db.conn.insert(tableName, it) } } + + /** Document ID one as a JSON string */ + static String one = '{"id":"one","value":"FIRST!","numValue":0,"sub":null}' + + /** Document ID two as a JSON string */ + static String two = '{"id":"two","value":"another","numValue":10,"sub":{"foo":"green","bar":"blue"}}' + + /** Document ID three as a JSON string */ + static String three = '{"id":"three","value":"","numValue":4,"sub":null}' + + /** Document ID four as a JSON string */ + static String four = '{"id":"four","value":"purple","numValue":17,"sub":{"foo":"green","bar":"red"}}' + + /** Document ID five as a JSON string */ + static String five = '{"id":"five","value":"purple","numValue":18,"sub":null}' } diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy index f657d5e..992c403 100644 --- a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy @@ -3,6 +3,18 @@ package solutions.bitbadger.documents.groovy.tests.integration import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +/** + * Tests for the JSON-returning functions + * + * NOTE: PostgreSQL JSONB columns do not preserve the original JSON with which a document was stored. These tests are + * the most complex within the library, as they have split testing based on the backing data store. The PostgreSQL tests + * check IDs (and, in the case of ordered queries, which ones occur before which others) vs. the entire JSON string. + * Meanwhile, SQLite stores JSON as text, and will return exactly the JSON it was given when it was originally written. + * These tests can ensure the expected round-trip of the entire JSON string. + */ final class JsonFunctions { /** @@ -13,8 +25,372 @@ final class JsonFunctions { * @return The actual expected JSON based on the database being tested */ static String maybeJsonB(String json) { - Configuration.dialect() == Dialect.SQLITE - ? json - : json.replace('":"', '": "').replace('","', '", "').replace('":[', '": [') + return switch (Configuration.dialect()) { + case Dialect.SQLITE -> json + case Dialect.POSTGRESQL -> json.replace ('":', '": ' ).replace (',"', ', "') + } } + + /** + * Create a snippet of JSON to find a document ID + * + * @param id The ID of the document + * @return A connection-aware ID to check for presence and positioning + */ + private static String docId(String id) { + return maybeJsonB("{\"id\":\"$id\"") + } + + static void allDefault(ThrowawayDatabase db) { + JsonDocument.load(db) + def json = db.conn.jsonAll(TEST_TABLE) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertTrue(json.contains(JsonDocument.one), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.two), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.three), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found in JSON ($json)") + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId("one")), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(docId("two")), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(docId("three")), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(docId("four")), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found in JSON ($json)") + break + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + static void allEmpty(ThrowawayDatabase db) = + assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") + + static void byIdString(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonById(TEST_TABLE, "two") + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "An incorrect document was returned ($json)") + } + } + + static void byIdNumber(ThrowawayDatabase db) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + assertEquals( + maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), db.conn.jsonById(TEST_TABLE, 18), + "The document should have been found by numeric ID" + ) + } finally { + Configuration.idField = "id" + } + } + + static void byIdNotFound(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + } + + static void byFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL + ) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals("[${JsonDocument.four}]", json, "The incorrect document was returned") + Dialect.POSTGRESQL -> { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("four")),"The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + } + + static void byFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + + Dialect.POSTGRESQL -> { + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + } + + static void byFieldsMatchNumIn(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals("[${JsonDocument.three}]", json, "The incorrect document was returned") + Dialect.POSTGRESQL -> { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("three")), "The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "[]", db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))), + "There should have been no documents returned" + ) + } + + static void byFieldsMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("first")), "The 'first' document was not found ($json)") + assertTrue(json.contains(docId("second")), "The 'second' document was not found ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + static void byFieldsNoMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + "[]", db.conn.jsonByFields( + TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) + ), "There should have been no documents returned" + ) + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "purple")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + static void byContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByContains( + TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.two},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + Dialect.POSTGRESQL -> { + val twoIdx = json.indexOf(docId("two")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(twoIdx >= 0, "Document 'two' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(twoIdx < fourIdx, "Document 'two' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no documents returned" + ) + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + static void byJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + + Dialect.POSTGRESQL -> { + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no documents returned" + ) + } + + static void firstByFieldsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "The incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "The incorrect document was returned ($json)") + } + } + + static void firstByFieldsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.two) || json.contains(JsonDocument.four), + "Expected document 'two' or 'four' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("two")) || json.contains(docId("four")), + "Expected document 'two' or 'four' ($json)" + ) + } + } + + static void firstByFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("four")), "An incorrect document was returned ($json)") + } + } + + static void firstByFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), + "There should have been no document returned" + ) + } + + static void firstByContainsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!")) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.one, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("one")), "An incorrect document was returned ($json)") + } + } + + static void firstByContainsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple")) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("four")) || json.contains(docId("five")), + "Expected document 'four' or 'five' ($json)" + ) + } + } + + static void firstByContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.five, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("five")), "An incorrect document was returned ($json)") + } + } + + static void firstByContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no document returned" + ) + } + + static void firstByJsonPathMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "An incorrect document was returned ($json)") + } + } + + static void firstByJsonPathMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("four")) || json.contains(docId("five")), + "Expected document 'four' or 'five' ($json)" + ) + } + } + + static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("four")), "An incorrect document was returned ($json)") + } + } + + static void firstByJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no document returned" + ) + } + }