|
|
@@ -0,0 +1,1033 @@
|
|
|
+using InvercargillSql;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Live PostgreSQL integration tests, mirroring sqlite-test.vala depth.
|
|
|
+ *
|
|
|
+ * Requires a reachable PostgreSQL server; the connection string comes
|
|
|
+ * from PG_TEST_CONN (default: unix socket dir /tmp/pgrun, port 5433,
|
|
|
+ * user postgres, database postgres).
|
|
|
+ *
|
|
|
+ * Each test owns its table: DROP TABLE IF EXISTS + CREATE keeps the
|
|
|
+ * suite self-contained and re-runnable against the shared database.
|
|
|
+ */
|
|
|
+public int main(string[] args) {
|
|
|
+ print("=== PostgreSQL Integration Tests ===\n\n");
|
|
|
+
|
|
|
+ string? conn_string = GLib.Environment.get_variable("PG_TEST_CONN");
|
|
|
+ if (conn_string == null || conn_string.length == 0) {
|
|
|
+ conn_string = "postgresql://postgres@/postgres?host=/tmp/pgrun&port=5433";
|
|
|
+ }
|
|
|
+ print("Using connection: %s\n\n", conn_string);
|
|
|
+
|
|
|
+ try {
|
|
|
+ print("--- Connection String Tests ---\n");
|
|
|
+ test_connection_string_parsing();
|
|
|
+ test_connection_factory_registration();
|
|
|
+ test_create_and_open_round_trip(conn_string);
|
|
|
+ test_invalid_scheme_rejected();
|
|
|
+ test_missing_database_rejected();
|
|
|
+ test_unreachable_server_rejected();
|
|
|
+
|
|
|
+ var conn = ConnectionFactory.create_and_open(conn_string);
|
|
|
+
|
|
|
+ print("\n--- Integer Element Tests ---\n");
|
|
|
+ test_integer_element_conversions(conn);
|
|
|
+ test_integer_element_bool_conversion(conn);
|
|
|
+ test_integer_element_null_handling(conn);
|
|
|
+
|
|
|
+ print("\n--- Real Element Tests ---\n");
|
|
|
+ test_real_element_conversions(conn);
|
|
|
+ test_real_element_null_handling(conn);
|
|
|
+
|
|
|
+ print("\n--- Text Element Tests ---\n");
|
|
|
+ test_text_element_conversions(conn);
|
|
|
+ test_text_element_bool_parsing(conn);
|
|
|
+ test_text_element_null_handling(conn);
|
|
|
+
|
|
|
+ print("\n--- Blob (bytea) Element Tests ---\n");
|
|
|
+ test_blob_element_round_trip(conn);
|
|
|
+ test_blob_element_empty_and_large(conn);
|
|
|
+ test_blob_element_null_handling(conn);
|
|
|
+
|
|
|
+ print("\n--- Null Element Tests ---\n");
|
|
|
+ test_null_element_handling(conn);
|
|
|
+
|
|
|
+ print("\n--- Round-trip Conversion Tests ---\n");
|
|
|
+ test_datetime_round_trip(conn);
|
|
|
+ test_datetime_timestamptz_round_trip(conn);
|
|
|
+ test_bool_round_trip(conn);
|
|
|
+ test_nullable_types_round_trip(conn);
|
|
|
+
|
|
|
+ print("\n--- CRUD / Fluent API Tests ---\n");
|
|
|
+ test_crud_insert_select_update_delete(conn);
|
|
|
+ test_execute_scalar(conn);
|
|
|
+ test_execute_non_query_changes(conn);
|
|
|
+ test_named_parameter_reuse(conn);
|
|
|
+
|
|
|
+ print("\n--- Transaction Tests ---\n");
|
|
|
+ test_transaction_commit_persists(conn);
|
|
|
+ test_transaction_rollback_discards(conn);
|
|
|
+ test_transaction_is_active(conn);
|
|
|
+
|
|
|
+ print("\n--- last_insert_rowid Tests ---\n");
|
|
|
+ test_last_insert_rowid(conn);
|
|
|
+ test_last_insert_rowid_in_transaction(conn);
|
|
|
+
|
|
|
+ print("\n--- Lazy Enumeration Tests ---\n");
|
|
|
+ test_lazy_enumeration_partial(conn);
|
|
|
+ test_peek_count(conn);
|
|
|
+ test_enumerable_cache(conn);
|
|
|
+
|
|
|
+ print("\n--- Error Path Tests ---\n");
|
|
|
+ test_syntax_error(conn);
|
|
|
+ test_unique_violation(conn);
|
|
|
+ test_missing_parameter(conn);
|
|
|
+
|
|
|
+ print("\n--- Mixed Integration Tests ---\n");
|
|
|
+ test_mixed_type_table(conn);
|
|
|
+ test_element_assignable_types(conn);
|
|
|
+
|
|
|
+ conn.close();
|
|
|
+ print("\n=== All PostgreSQL integration tests passed! ===\n");
|
|
|
+ return 0;
|
|
|
+ } catch (Error e) {
|
|
|
+ printerr("\n=== Test failed: %s ===\n", e.message);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Connection string tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+string CONN() {
|
|
|
+ string? cs = GLib.Environment.get_variable("PG_TEST_CONN");
|
|
|
+ return (cs == null || cs.length == 0)
|
|
|
+ ? "postgresql://postgres@/postgres?host=/tmp/pgrun&port=5433"
|
|
|
+ : cs;
|
|
|
+}
|
|
|
+
|
|
|
+void test_connection_string_parsing() throws SqlError {
|
|
|
+ print("Test: connection string parsing... ");
|
|
|
+
|
|
|
+ var cs = ConnectionString.parse("postgresql://user:secret@dbhost:5432/mydb");
|
|
|
+ assert(cs.scheme == "postgresql");
|
|
|
+ assert(cs.user == "user");
|
|
|
+ assert(cs.password == "secret");
|
|
|
+ assert(cs.host == "dbhost");
|
|
|
+ assert(cs.port == 5432);
|
|
|
+ assert(cs.database == "mydb");
|
|
|
+
|
|
|
+ // Short scheme alias normalises to postgresql.
|
|
|
+ var cs2 = ConnectionString.parse("postgres://bob@host/db");
|
|
|
+ assert(cs2.scheme == "postgres");
|
|
|
+ assert(cs2.user == "bob");
|
|
|
+ assert(cs2.host == "host");
|
|
|
+ assert(cs2.database == "db");
|
|
|
+
|
|
|
+ // Unix socket dir via host= option (empty URI host).
|
|
|
+ var cs3 = ConnectionString.parse("postgresql://postgres@/postgres?host=/tmp/pgrun&port=5433");
|
|
|
+ assert(cs3.database == "postgres");
|
|
|
+ assert(cs3.has_option("host"));
|
|
|
+ assert(cs3.get_option("host") == "/tmp/pgrun");
|
|
|
+ assert(cs3.has_option("port"));
|
|
|
+ assert(cs3.get_option("port") == "5433");
|
|
|
+
|
|
|
+ // IPv6 form parses without throwing.
|
|
|
+ var cs4 = ConnectionString.parse("postgresql://u@[::1]:5433/d");
|
|
|
+ assert(cs4.host == "::1");
|
|
|
+ assert(cs4.port == 5433);
|
|
|
+
|
|
|
+ // Invalid port is rejected at parse time.
|
|
|
+ try {
|
|
|
+ ConnectionString.parse("postgresql://u@h:notaport/d");
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.INVALID_CONNECTION_STRING);
|
|
|
+ }
|
|
|
+
|
|
|
+ print("PASSED\n");
|
|
|
+}
|
|
|
+
|
|
|
+void test_connection_factory_registration() throws SqlError {
|
|
|
+ print("Test: ConnectionFactory provider registration... ");
|
|
|
+
|
|
|
+ // Providers register lazily on first use of their scheme. Creating
|
|
|
+ // a postgres connection must make both spellings report present.
|
|
|
+ assert(!ConnectionFactory.has_provider("mysql"));
|
|
|
+ ConnectionFactory.create(CONN());
|
|
|
+ assert(ConnectionFactory.has_provider("postgresql"));
|
|
|
+ assert(ConnectionFactory.has_provider("postgres"));
|
|
|
+ // sqlite registers on first sqlite use, not as a side effect of
|
|
|
+ // postgres connections (factory keeps one flag per scheme).
|
|
|
+ ConnectionFactory.create("sqlite::memory:");
|
|
|
+ assert(ConnectionFactory.has_provider("sqlite"));
|
|
|
+
|
|
|
+ print("PASSED\n");
|
|
|
+}
|
|
|
+
|
|
|
+void test_create_and_open_round_trip(string conn_string) throws SqlError {
|
|
|
+ print("Test: create_and_open round trip... ");
|
|
|
+
|
|
|
+ var conn = ConnectionFactory.create_and_open(conn_string);
|
|
|
+ assert(conn.is_open);
|
|
|
+
|
|
|
+ // A live query proves the socket is actually usable.
|
|
|
+ var scalar = conn.create_command("SELECT 42").execute_scalar();
|
|
|
+ assert(scalar != null);
|
|
|
+ int64? v = null;
|
|
|
+ assert(scalar.try_get_as<int64?>(out v));
|
|
|
+ assert(v == 42);
|
|
|
+
|
|
|
+ conn.close();
|
|
|
+ assert(!conn.is_open);
|
|
|
+
|
|
|
+ print("PASSED\n");
|
|
|
+}
|
|
|
+
|
|
|
+void test_invalid_scheme_rejected() throws SqlError {
|
|
|
+ print("Test: invalid scheme rejected... ");
|
|
|
+
|
|
|
+ try {
|
|
|
+ ConnectionFactory.create_and_open("mysql://user@host/db");
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.INVALID_CONNECTION_STRING);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Missing scheme entirely.
|
|
|
+ try {
|
|
|
+ ConnectionFactory.create_and_open("nonsense");
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.INVALID_CONNECTION_STRING);
|
|
|
+ }
|
|
|
+
|
|
|
+ print("PASSED\n");
|
|
|
+}
|
|
|
+
|
|
|
+void test_missing_database_rejected() throws SqlError {
|
|
|
+ print("Test: missing database rejected... ");
|
|
|
+
|
|
|
+ // No database component at all -> parse error.
|
|
|
+ try {
|
|
|
+ ConnectionFactory.create_and_open("postgresql://user@host");
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.INVALID_CONNECTION_STRING);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Empty database with trailing slash -> provider-level error.
|
|
|
+ try {
|
|
|
+ ConnectionFactory.create_and_open("postgresql://user@host/");
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.INVALID_CONNECTION_STRING);
|
|
|
+ }
|
|
|
+
|
|
|
+ print("PASSED\n");
|
|
|
+}
|
|
|
+
|
|
|
+void test_unreachable_server_rejected() throws SqlError {
|
|
|
+ print("Test: unreachable server rejected... ");
|
|
|
+
|
|
|
+ // Port 1 on loopback: nothing listens there in test containers.
|
|
|
+ try {
|
|
|
+ ConnectionFactory.create_and_open(
|
|
|
+ "postgresql://postgres@127.0.0.1:1/postgres?connect_timeout=2"
|
|
|
+ );
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.CONNECTION_FAILED);
|
|
|
+ }
|
|
|
+
|
|
|
+ print("PASSED\n");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Helpers
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+/** Prints the per-test success marker with context. */
|
|
|
+void pass(string what) {
|
|
|
+ print("PASSED (%s)\n", what);
|
|
|
+}
|
|
|
+
|
|
|
+/** Runs a single-parameter command (fluent API) and returns rows affected. */
|
|
|
+int exec_p<T>(Connection conn, string sql, string name, T value) throws SqlError {
|
|
|
+ return conn.create_command(sql).with_parameter<T>(name, value).execute_non_query();
|
|
|
+}
|
|
|
+
|
|
|
+/** Drops table if present, then runs the CREATE TABLE ddl. */
|
|
|
+void drop_and_create(Connection conn, string ddl) throws SqlError {
|
|
|
+ int start = ddl.up().index_of("TABLE ") + 6;
|
|
|
+ int end = ddl.index_of("(", start);
|
|
|
+ string table = ddl.substring(start, end - start).strip();
|
|
|
+ conn.execute("DROP TABLE IF EXISTS " + table);
|
|
|
+ conn.execute(ddl);
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Integer element tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_integer_element_conversions(Connection conn) throws SqlError {
|
|
|
+ print("Test: Integer element conversions... ");
|
|
|
+ drop_and_create(conn, """
|
|
|
+ CREATE TABLE test_int (
|
|
|
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
|
+ value BIGINT)
|
|
|
+ """);
|
|
|
+ string ins = "INSERT INTO test_int (value) VALUES (:val)";
|
|
|
+ exec_p<int64?>(conn, ins, "val", 42);
|
|
|
+ exec_p<int64?>(conn, ins, "val", -123);
|
|
|
+ exec_p<int64?>(conn, ins, "val", int64.MAX);
|
|
|
+
|
|
|
+ var array = conn.create_command("SELECT value FROM test_int ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 3);
|
|
|
+
|
|
|
+ var elem = array[0].get("value");
|
|
|
+ int64? v64 = null;
|
|
|
+ int? vi = null;
|
|
|
+ double? vd = null;
|
|
|
+ float? vf = null;
|
|
|
+ string? vs = null;
|
|
|
+ assert(elem.try_get_as<int64?>(out v64) && v64 == 42);
|
|
|
+ assert(elem.try_get_as<int?>(out vi) && vi == 42);
|
|
|
+ assert(elem.try_get_as<double?>(out vd) && vd == 42.0);
|
|
|
+ assert(elem.try_get_as<float?>(out vf) && vf >= 41.9f && vf <= 42.1f);
|
|
|
+ assert(elem.try_get_as<string>(out vs) && vs == "42");
|
|
|
+
|
|
|
+ assert(array[1].get("value").try_get_as<int64?>(out v64) && v64 == -123);
|
|
|
+ assert(array[2].get("value").try_get_as<int64?>(out v64) && v64 == int64.MAX);
|
|
|
+ pass("int conversions");
|
|
|
+}
|
|
|
+
|
|
|
+void test_integer_element_bool_conversion(Connection conn) throws SqlError {
|
|
|
+ print("Test: Integer element bool conversion... ");
|
|
|
+ drop_and_create(conn, """
|
|
|
+ CREATE TABLE test_int_bool (
|
|
|
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
|
+ value BIGINT)
|
|
|
+ """);
|
|
|
+ string ins = "INSERT INTO test_int_bool (value) VALUES (:v)";
|
|
|
+ exec_p<int64?>(conn, ins, "v", 1);
|
|
|
+ exec_p<int64?>(conn, ins, "v", 0);
|
|
|
+
|
|
|
+ var array = conn.create_command("SELECT value FROM test_int_bool ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 2);
|
|
|
+ bool? b = null;
|
|
|
+ assert(array[0].get("value").try_get_as<bool?>(out b) && b == true);
|
|
|
+ assert(array[1].get("value").try_get_as<bool?>(out b) && b == false);
|
|
|
+ pass("int->bool");
|
|
|
+}
|
|
|
+
|
|
|
+void test_integer_element_null_handling(Connection conn) throws SqlError {
|
|
|
+ print("Test: Integer element null handling... ");
|
|
|
+ drop_and_create(conn, """
|
|
|
+ CREATE TABLE test_int_null (
|
|
|
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
|
+ value BIGINT)
|
|
|
+ """);
|
|
|
+ conn.create_command("INSERT INTO test_int_null (value) VALUES (:v)")
|
|
|
+ .with_null("v").execute_non_query();
|
|
|
+
|
|
|
+ var array = conn.create_command("SELECT value FROM test_int_null")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 1);
|
|
|
+ var elem = array[0].get("value");
|
|
|
+ assert(elem != null && elem.is_null());
|
|
|
+ int64? v = null;
|
|
|
+ assert(elem.try_get_as<int64?>(out v) == false);
|
|
|
+ assert(elem.assignable_to_type(typeof(int64?)) == true);
|
|
|
+ pass("int null");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Real element tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_real_element_conversions(Connection conn) throws SqlError {
|
|
|
+ print("Test: Real element conversions... ");
|
|
|
+ drop_and_create(conn, """
|
|
|
+ CREATE TABLE test_real (
|
|
|
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
|
+ value DOUBLE PRECISION)
|
|
|
+ """);
|
|
|
+ string ins = "INSERT INTO test_real (value) VALUES (:v)";
|
|
|
+ exec_p<double?>(conn, ins, "v", 3.14159);
|
|
|
+ exec_p<double?>(conn, ins, "v", -0.0001);
|
|
|
+ exec_p<double?>(conn, ins, "v", 1.0e300);
|
|
|
+
|
|
|
+ var array = conn.create_command("SELECT value FROM test_real ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 3);
|
|
|
+ double? d = null;
|
|
|
+ assert(array[0].get("value").try_get_as<double?>(out d));
|
|
|
+ assert(d >= 3.14158 && d <= 3.14160);
|
|
|
+ assert(array[1].get("value").try_get_as<double?>(out d));
|
|
|
+ assert(d >= -0.00011 && d <= -0.00009);
|
|
|
+ assert(array[2].get("value").try_get_as<double?>(out d));
|
|
|
+ assert(d >= 0.99e300 && d <= 1.01e300);
|
|
|
+
|
|
|
+ string? s = null;
|
|
|
+ assert(array[0].get("value").try_get_as<string>(out s));
|
|
|
+ assert(double.parse(s) >= 3.14158 && double.parse(s) <= 3.14160);
|
|
|
+ float? f = null;
|
|
|
+ assert(array[0].get("value").try_get_as<float?>(out f));
|
|
|
+ assert(f >= 3.1415f && f <= 3.1417f);
|
|
|
+ pass("real conversions");
|
|
|
+}
|
|
|
+
|
|
|
+void test_real_element_null_handling(Connection conn) throws SqlError {
|
|
|
+ print("Test: Real element null handling... ");
|
|
|
+ drop_and_create(conn, """
|
|
|
+ CREATE TABLE test_real_null (
|
|
|
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
|
+ value DOUBLE PRECISION)
|
|
|
+ """);
|
|
|
+ conn.execute("INSERT INTO test_real_null (value) VALUES (NULL)");
|
|
|
+ var array = conn.create_command("SELECT value FROM test_real_null")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 1);
|
|
|
+ var elem = array[0].get("value");
|
|
|
+ assert(elem.is_null() == true);
|
|
|
+ double? d = null;
|
|
|
+ assert(elem.try_get_as<double?>(out d) == false);
|
|
|
+ assert(elem.assignable_to_type(typeof(double?)) == true);
|
|
|
+ pass("real null");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Text element tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_text_element_conversions(Connection conn) throws SqlError {
|
|
|
+ print("Test: Text element conversions... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_text (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, value TEXT)");
|
|
|
+ string[] samples = { "hello", "", "with 'quotes'", "üñïçødé", "line1\nline2" };
|
|
|
+ foreach (string sample in samples) {
|
|
|
+ exec_p<string>(conn, "INSERT INTO test_text (value) VALUES (:v)", "v", sample);
|
|
|
+ }
|
|
|
+ var array = conn.create_command("SELECT value FROM test_text ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == samples.length);
|
|
|
+ for (int i = 0; i < samples.length; i++) {
|
|
|
+ string? s = null;
|
|
|
+ assert(array[i].get("value").try_get_as<string>(out s));
|
|
|
+ assert(s == samples[i]);
|
|
|
+ }
|
|
|
+ conn.execute("DELETE FROM test_text");
|
|
|
+ exec_p<string>(conn, "INSERT INTO test_text (value) VALUES (:v)", "v", "12345");
|
|
|
+ var elem = conn.create_command("SELECT value FROM test_text").execute_scalar();
|
|
|
+ int64? n = null;
|
|
|
+ assert(elem.try_get_as<int64?>(out n) && n == 12345);
|
|
|
+ pass("text conversions");
|
|
|
+}
|
|
|
+
|
|
|
+void test_text_element_bool_parsing(Connection conn) throws SqlError {
|
|
|
+ print("Test: Text element bool parsing... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_text_bool (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, value TEXT)");
|
|
|
+ exec_p<string>(conn, "INSERT INTO test_text_bool (value) VALUES (:v)", "v", "true");
|
|
|
+ exec_p<string>(conn, "INSERT INTO test_text_bool (value) VALUES (:v)", "v", "false");
|
|
|
+ var array = conn.create_command("SELECT value FROM test_text_bool ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 2);
|
|
|
+ bool? b = null;
|
|
|
+ assert(array[0].get("value").try_get_as<bool?>(out b) && b == true);
|
|
|
+ assert(array[1].get("value").try_get_as<bool?>(out b) && b == false);
|
|
|
+ pass("text bool");
|
|
|
+}
|
|
|
+
|
|
|
+void test_text_element_null_handling(Connection conn) throws SqlError {
|
|
|
+ print("Test: Text element null handling... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_text_null (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, value TEXT)");
|
|
|
+ conn.create_command("INSERT INTO test_text_null (value) VALUES (:v)")
|
|
|
+ .with_parameter<string?>("v", null).execute_non_query();
|
|
|
+ var array = conn.create_command("SELECT value FROM test_text_null")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 1);
|
|
|
+ var elem = array[0].get("value");
|
|
|
+ assert(elem.is_null());
|
|
|
+ string? s = null;
|
|
|
+ assert(elem.try_get_as<string>(out s) == false);
|
|
|
+ pass("text null");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Blob (bytea) element tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+Invercargill.BinaryData wrap_bytes(uint8[] bytes) {
|
|
|
+ return Invercargill.Wrap.byte_array(bytes);
|
|
|
+}
|
|
|
+
|
|
|
+void test_blob_element_round_trip(Connection conn) throws SqlError {
|
|
|
+ print("Test: Blob element round trip... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_blob (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, data BYTEA)");
|
|
|
+ uint8[] bytes = { 0x00, 0x01, 0x02, 0xFE, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF };
|
|
|
+ exec_p<Invercargill.BinaryData>(conn, "INSERT INTO test_blob (data) VALUES (:d)", "d", wrap_bytes(bytes));
|
|
|
+ var elem = conn.create_command("SELECT data FROM test_blob").execute_scalar();
|
|
|
+ assert(elem != null && !elem.is_null());
|
|
|
+ Invercargill.BinaryData? out_data = null;
|
|
|
+ assert(elem.try_get_as<Invercargill.BinaryData>(out out_data));
|
|
|
+ assert(out_data != null);
|
|
|
+ uint8[] out_bytes = out_data.to_array();
|
|
|
+ assert(out_bytes.length == bytes.length);
|
|
|
+ for (int i = 0; i < out_bytes.length; i++) {
|
|
|
+ assert(out_bytes[i] == bytes[i]);
|
|
|
+ }
|
|
|
+ pass("blob round trip");
|
|
|
+}
|
|
|
+
|
|
|
+void test_blob_element_empty_and_large(Connection conn) throws SqlError {
|
|
|
+ print("Test: Blob empty and large... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_blob_sizes (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, data BYTEA)");
|
|
|
+ exec_p<Invercargill.BinaryData>(conn, "INSERT INTO test_blob_sizes (data) VALUES (:d)", "d", wrap_bytes(new uint8[0]));
|
|
|
+ var big = new uint8[256 * 1024];
|
|
|
+ for (int i = 0; i < big.length; i++) {
|
|
|
+ big[i] = (uint8)(i % 251);
|
|
|
+ }
|
|
|
+ exec_p<Invercargill.BinaryData>(conn, "INSERT INTO test_blob_sizes (data) VALUES (:d)", "d", wrap_bytes(big));
|
|
|
+ var array = conn.create_command(
|
|
|
+ "SELECT data, octet_length(data) AS byte_count FROM test_blob_sizes ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 2);
|
|
|
+ // Row 0: empty blob — stored as empty, distinct from NULL.
|
|
|
+ int64? stored_len = null;
|
|
|
+ assert(array[0].get("byte_count").try_get_as<int64?>(out stored_len) && stored_len == 0);
|
|
|
+ var empty_elem = array[0].get("data");
|
|
|
+ Invercargill.BinaryData? empty_out = null;
|
|
|
+ if (empty_elem.try_get_as<Invercargill.BinaryData>(out empty_out) && empty_out != null) {
|
|
|
+ assert(empty_out.to_array().length == 0);
|
|
|
+ } else {
|
|
|
+ // Adapter maps empty bytea to null payload (sqlite parity);
|
|
|
+ // octet_length == 0 already proved empty-not-NULL.
|
|
|
+ assert(empty_elem.is_null());
|
|
|
+ }
|
|
|
+ // Row 1: large blob round trips with content intact.
|
|
|
+ var big_elem = array[1].get("data");
|
|
|
+ assert(!big_elem.is_null());
|
|
|
+ assert(array[1].get("byte_count").try_get_as<int64?>(out stored_len) && stored_len == big.length);
|
|
|
+ Invercargill.BinaryData? big_out = null;
|
|
|
+ assert(big_elem.try_get_as<Invercargill.BinaryData>(out big_out));
|
|
|
+ uint8[] round = big_out.to_array();
|
|
|
+ assert(round.length == big.length);
|
|
|
+ for (int i = 0; i < round.length; i += 997) {
|
|
|
+ assert(round[i] == big[i]);
|
|
|
+ }
|
|
|
+ pass("blob sizes");
|
|
|
+}
|
|
|
+
|
|
|
+void test_blob_element_null_handling(Connection conn) throws SqlError {
|
|
|
+ print("Test: Blob element null handling... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_blob_null (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, data BYTEA)");
|
|
|
+ conn.create_command("INSERT INTO test_blob_null (data) VALUES (:d)")
|
|
|
+ .with_null("d").execute_non_query();
|
|
|
+ var array = conn.create_command("SELECT data FROM test_blob_null")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 1);
|
|
|
+ assert(array[0].get("data").is_null());
|
|
|
+ pass("blob null");
|
|
|
+}
|
|
|
+
|
|
|
+void test_null_element_handling(Connection conn) throws SqlError {
|
|
|
+ print("Test: Null element handling across types... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_nulls (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, i BIGINT, r DOUBLE PRECISION, t TEXT, b BYTEA, f BOOLEAN)");
|
|
|
+ conn.create_command("INSERT INTO test_nulls (i, r, t, b, f) VALUES (:i, :r, :t, :b, :f)")
|
|
|
+ .with_null("i").with_null("r").with_null("t").with_null("b").with_null("f")
|
|
|
+ .execute_non_query();
|
|
|
+ var array = conn.create_command("SELECT i, r, t, b, f FROM test_nulls")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 1);
|
|
|
+ foreach (string col in new string[] { "i", "r", "t", "b", "f" }) {
|
|
|
+ var elem = array[0].get(col);
|
|
|
+ assert(elem != null && elem.is_null());
|
|
|
+ assert(elem.assignable_to_type(typeof(int64?)) == true);
|
|
|
+ assert(elem.assignable_to_type(typeof(string)) == true);
|
|
|
+ }
|
|
|
+ pass("null elements");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// DateTime round-trips
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_datetime_round_trip(Connection conn) throws SqlError {
|
|
|
+ print("Test: DateTime round trip (stored as text)... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_dt_text (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, created_at TEXT)");
|
|
|
+ var original = test_dt_source();
|
|
|
+ exec_p<DateTime>(conn, "INSERT INTO test_dt_text (created_at) VALUES (:ts)", "ts", original);
|
|
|
+ var elem = conn.create_command("SELECT created_at FROM test_dt_text").execute_scalar();
|
|
|
+ assert(elem != null);
|
|
|
+ // A TEXT column comes back as a text element; the adapter does not
|
|
|
+ // guess datetime from arbitrary text, so fidelity is checked by
|
|
|
+ // parsing the stored ISO 8601 string (that is what was bound).
|
|
|
+ string? stored = null;
|
|
|
+ assert(elem.try_get_as<string>(out stored));
|
|
|
+ assert(stored != null);
|
|
|
+ var back = new DateTime.from_iso8601(stored, new TimeZone.local());
|
|
|
+ assert(back != null);
|
|
|
+ assert(back.to_unix() == original.to_unix());
|
|
|
+ pass("datetime text");
|
|
|
+}
|
|
|
+
|
|
|
+void test_datetime_timestamptz_round_trip(Connection conn) throws SqlError {
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_dt_tz (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, created_at TIMESTAMPTZ)");
|
|
|
+ exec_p<DateTime>(conn, "INSERT INTO test_dt_tz (created_at) VALUES (:ts)", "ts", test_dt_source());
|
|
|
+ var elem = conn.create_command("SELECT created_at FROM test_dt_tz").execute_scalar();
|
|
|
+ assert(elem != null);
|
|
|
+ DateTime? back = null;
|
|
|
+ assert(elem.try_get_as<DateTime>(out back));
|
|
|
+ assert(back != null);
|
|
|
+ assert(back.to_unix() == test_dt_source().to_unix());
|
|
|
+ pass("datetime timestamptz");
|
|
|
+}
|
|
|
+
|
|
|
+/** Stable timestamp source so insert and verify compare the same instant. */
|
|
|
+DateTime test_dt_source() {
|
|
|
+ return new DateTime.from_unix_local(1718459445);
|
|
|
+}
|
|
|
+
|
|
|
+void test_bool_round_trip(Connection conn) throws SqlError {
|
|
|
+ print("Test: Bool round trip (native boolean)... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_bool (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, is_active BOOLEAN)");
|
|
|
+ exec_p<bool>(conn, "INSERT INTO test_bool (is_active) VALUES (:v)", "v", true);
|
|
|
+ exec_p<bool>(conn, "INSERT INTO test_bool (is_active) VALUES (:v)", "v", false);
|
|
|
+ var array = conn.create_command("SELECT is_active FROM test_bool ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 2);
|
|
|
+ bool? b = null;
|
|
|
+ assert(array[0].get("is_active").try_get_as<bool?>(out b) && b == true);
|
|
|
+ assert(array[1].get("is_active").try_get_as<bool?>(out b) && b == false);
|
|
|
+ pass("bool round trip");
|
|
|
+}
|
|
|
+
|
|
|
+void test_nullable_types_round_trip(Connection conn) throws SqlError {
|
|
|
+ print("Test: Nullable types round trip... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE test_nullable (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, int_val BIGINT, real_val DOUBLE PRECISION, str_val TEXT)");
|
|
|
+ double? r_param = 3.14;
|
|
|
+ conn.create_command("INSERT INTO test_nullable (int_val, real_val, str_val) VALUES (:i, :r, :s)")
|
|
|
+ .with_parameter("i", 42)
|
|
|
+ .with_parameter<double?>("r", r_param)
|
|
|
+ .with_parameter("s", "hello")
|
|
|
+ .execute_non_query();
|
|
|
+ conn.execute("INSERT INTO test_nullable (int_val, real_val, str_val) VALUES (NULL, NULL, NULL)");
|
|
|
+ var array = conn.create_command(
|
|
|
+ "SELECT int_val, real_val, str_val FROM test_nullable ORDER BY id")
|
|
|
+ .execute_query().to_array();
|
|
|
+ assert(array.length == 2);
|
|
|
+ int64? i = null;
|
|
|
+ double? r = null;
|
|
|
+ string? s = null;
|
|
|
+ assert(array[0].get("int_val").try_get_as<int64?>(out i) && i == 42);
|
|
|
+ assert(array[0].get("real_val").try_get_as<double?>(out r));
|
|
|
+ assert(r >= 3.13 && r <= 3.15);
|
|
|
+ assert(array[0].get("str_val").try_get_as<string>(out s) && s == "hello");
|
|
|
+ assert(array[1].get("int_val").is_null());
|
|
|
+ assert(array[1].get("real_val").is_null());
|
|
|
+ assert(array[1].get("str_val").is_null());
|
|
|
+ pass("nullable types");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// CRUD / fluent API tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_crud_insert_select_update_delete(Connection conn) throws SqlError {
|
|
|
+ print("Test: CRUD insert/select/update/delete... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE crud_people (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, age INT)");
|
|
|
+ string ins = "INSERT INTO crud_people (name, age) VALUES (:n, :a)";
|
|
|
+ conn.create_command(ins)
|
|
|
+ .with_parameter("n", "Alice").with_parameter("a", 30).execute_non_query();
|
|
|
+ conn.create_command("INSERT INTO crud_people (name, age) VALUES (:n, :a)")
|
|
|
+ .with_parameter("n", "Bob").with_parameter("a", 25).execute_non_query();
|
|
|
+ conn.create_command("INSERT INTO crud_people (name, age) VALUES (:n, :a)")
|
|
|
+ .with_parameter("n", "Carol").with_parameter("a", 35).execute_non_query();
|
|
|
+
|
|
|
+ // SELECT with named parameter in WHERE.
|
|
|
+ var found = conn.create_command("SELECT name, age FROM crud_people WHERE age > :min_age ORDER BY age")
|
|
|
+ .with_parameter("min_age", 26).execute_query().to_array();
|
|
|
+ assert(found.length == 2);
|
|
|
+ string? nm = null;
|
|
|
+ assert(found[0].get("name").try_get_as<string>(out nm) && nm == "Alice");
|
|
|
+ assert(found[1].get("name").try_get_as<string>(out nm) && nm == "Carol");
|
|
|
+
|
|
|
+ // UPDATE with parameter, verify affected-rows count.
|
|
|
+ int updated = conn.create_command("UPDATE crud_people SET age = :new_age WHERE name = :who")
|
|
|
+ .with_parameter("new_age", 31).with_parameter("who", "Alice").execute_non_query();
|
|
|
+ assert(updated == 1);
|
|
|
+ var check = conn.create_command("SELECT age FROM crud_people WHERE name = 'Alice'").execute_scalar();
|
|
|
+ int64? a = null;
|
|
|
+ assert(check.try_get_as<int64?>(out a) && a == 31);
|
|
|
+
|
|
|
+ // DELETE with parameter.
|
|
|
+ int deleted = conn.create_command("DELETE FROM crud_people WHERE age < :cutoff")
|
|
|
+ .with_parameter("cutoff", 30).execute_non_query();
|
|
|
+ assert(deleted == 1);
|
|
|
+ var count_elem = conn.create_command("SELECT count(*) AS n FROM crud_people").execute_scalar();
|
|
|
+ int64? remaining = null;
|
|
|
+ assert(count_elem.try_get_as<int64?>(out remaining) && remaining == 2);
|
|
|
+ pass("CRUD");
|
|
|
+}
|
|
|
+
|
|
|
+void test_execute_scalar(Connection conn) throws SqlError {
|
|
|
+ print("Test: execute_scalar variants... ");
|
|
|
+ var forty_two = conn.create_command("SELECT 42 AS v").execute_scalar();
|
|
|
+ int64? v = null;
|
|
|
+ assert(forty_two.try_get_as<int64?>(out v) && v == 42);
|
|
|
+
|
|
|
+ // Scalar over an aggregate.
|
|
|
+ var total = conn.create_command("SELECT 1 + 2 + 3 AS sum").execute_scalar();
|
|
|
+ assert(total.try_get_as<int64?>(out v) && v == 6);
|
|
|
+
|
|
|
+ // No rows -> null element, no crash.
|
|
|
+ var none = conn.create_command("SELECT 1 AS v WHERE 1 = 0").execute_scalar();
|
|
|
+ assert(none == null);
|
|
|
+
|
|
|
+ // Scalar with parameter.
|
|
|
+ var doubled = conn.create_command("SELECT :x * 2 AS d").with_parameter("x", 21).execute_scalar();
|
|
|
+ assert(doubled.try_get_as<int64?>(out v) && v == 42);
|
|
|
+ pass("execute_scalar");
|
|
|
+}
|
|
|
+
|
|
|
+void test_execute_non_query_changes(Connection conn) throws SqlError {
|
|
|
+ print("Test: execute_non_query row counts... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE changes_probe (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, flag BOOLEAN NOT NULL)");
|
|
|
+ exec_p<bool>(conn, "INSERT INTO changes_probe (flag) VALUES (:f)", "f", true);
|
|
|
+ exec_p<bool>(conn, "INSERT INTO changes_probe (flag) VALUES (:f)", "f", false);
|
|
|
+ exec_p<bool>(conn, "INSERT INTO changes_probe (flag) VALUES (:f)", "f", true);
|
|
|
+ int flipped = conn.create_command("UPDATE changes_probe SET flag = NOT flag").execute_non_query();
|
|
|
+ assert(flipped == 3);
|
|
|
+ // Flags were inserted true, false, true; NOT flips them to
|
|
|
+ // false, true, false, so exactly one row matches "WHERE flag".
|
|
|
+ int removed = conn.create_command("DELETE FROM changes_probe WHERE flag").execute_non_query();
|
|
|
+ assert(removed == 1);
|
|
|
+ // Row counts read through the connection's changes counter.
|
|
|
+ var remaining = conn.create_command("SELECT count(*) AS c FROM changes_probe").execute_scalar();
|
|
|
+ int64? left = null;
|
|
|
+ assert(remaining.try_get_as<int64?>(out left) && left == 2);
|
|
|
+ pass("non_query counts");
|
|
|
+}
|
|
|
+
|
|
|
+void test_named_parameter_reuse(Connection conn) throws SqlError {
|
|
|
+ print("Test: named parameter reuse in one statement... ");
|
|
|
+ // Same :x twice: translator must map both to $1.
|
|
|
+ var elem = conn.create_command("SELECT :x AS a, :x AS b, :x AS c")
|
|
|
+ .with_parameter("x", 7).execute_scalar();
|
|
|
+ int64? v = null;
|
|
|
+ assert(elem.try_get_as<int64?>(out v) && v == 7);
|
|
|
+ var array = conn.create_command("SELECT :x AS a, :x AS b, :x AS c")
|
|
|
+ .with_parameter("x", 7).execute_query().to_array();
|
|
|
+ assert(array.length == 1);
|
|
|
+ assert(array[0].get("b").try_get_as<int64?>(out v) && v == 7);
|
|
|
+ assert(array[0].get("c").try_get_as<int64?>(out v) && v == 7);
|
|
|
+ pass("parameter reuse");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Transaction tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_transaction_commit_persists(Connection conn) throws SqlError {
|
|
|
+ print("Test: transaction commit persists... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE txn_commit (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, payload TEXT)");
|
|
|
+ var tx = conn.begin_transaction();
|
|
|
+ assert(tx.is_active);
|
|
|
+ exec_p<string>(conn, "INSERT INTO txn_commit (payload) VALUES (:p)", "p", "committed-row");
|
|
|
+ tx.commit();
|
|
|
+ assert(!tx.is_active);
|
|
|
+ var n = conn.create_command("SELECT count(*) AS c FROM txn_commit").execute_scalar();
|
|
|
+ int64? c = null;
|
|
|
+ assert(n.try_get_as<int64?>(out c) && c == 1);
|
|
|
+ pass("commit persists");
|
|
|
+}
|
|
|
+
|
|
|
+void test_transaction_rollback_discards(Connection conn) throws SqlError {
|
|
|
+ print("Test: transaction rollback discards... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE txn_rollback (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, payload TEXT)");
|
|
|
+ var tx = conn.begin_transaction();
|
|
|
+ exec_p<string>(conn, "INSERT INTO txn_rollback (payload) VALUES (:p)", "p", "doomed-row");
|
|
|
+ tx.rollback();
|
|
|
+ assert(!tx.is_active);
|
|
|
+ var n = conn.create_command("SELECT count(*) AS c FROM txn_rollback").execute_scalar();
|
|
|
+ int64? c = null;
|
|
|
+ assert(n.try_get_as<int64?>(out c) && c == 0);
|
|
|
+ pass("rollback discards");
|
|
|
+}
|
|
|
+
|
|
|
+void test_transaction_is_active(Connection conn) throws SqlError {
|
|
|
+ print("Test: transaction double-commit/rollback rejected... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE txn_active (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, v INT)");
|
|
|
+ var tx = conn.begin_transaction();
|
|
|
+ exec_p<int>(conn, "INSERT INTO txn_active (v) VALUES (:v)", "v", 1);
|
|
|
+ tx.commit();
|
|
|
+ try {
|
|
|
+ tx.commit();
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.TRANSACTION_FAILED);
|
|
|
+ }
|
|
|
+ var tx2 = conn.begin_transaction();
|
|
|
+ exec_p<int>(conn, "INSERT INTO txn_active (v) VALUES (:v)", "v", 2);
|
|
|
+ tx2.rollback();
|
|
|
+ try {
|
|
|
+ tx2.rollback();
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.TRANSACTION_FAILED);
|
|
|
+ }
|
|
|
+ var n = conn.create_command("SELECT count(*) AS c FROM txn_active").execute_scalar();
|
|
|
+ int64? c = null;
|
|
|
+ assert(n.try_get_as<int64?>(out c) && c == 1);
|
|
|
+ pass("txn lifecycle");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// last_insert_rowid tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_last_insert_rowid(Connection conn) throws SqlError {
|
|
|
+ print("Test: last_insert_rowid after INSERT (identity)... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE rowid_probe (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, label TEXT)");
|
|
|
+ exec_p<string>(conn, "INSERT INTO rowid_probe (label) VALUES (:l)", "l", "first");
|
|
|
+ int64 first_id = conn.last_insert_rowid;
|
|
|
+ assert(first_id > 0);
|
|
|
+ exec_p<string>(conn, "INSERT INTO rowid_probe (label) VALUES (:l)", "l", "second");
|
|
|
+ int64 second_id = conn.last_insert_rowid;
|
|
|
+ assert(second_id > first_id);
|
|
|
+ // The ids must actually key back to the rows we inserted.
|
|
|
+ var label = conn.create_command("SELECT label FROM rowid_probe WHERE id = :i")
|
|
|
+ .with_parameter<int64?>("i", first_id).execute_scalar();
|
|
|
+ string? l = null;
|
|
|
+ assert(label.try_get_as<string>(out l) && l == "first");
|
|
|
+ var label2 = conn.create_command("SELECT label FROM rowid_probe WHERE id = :i")
|
|
|
+ .with_parameter<int64?>("i", second_id).execute_scalar();
|
|
|
+ assert(label2.try_get_as<string>(out l) && l == "second");
|
|
|
+ pass("last_insert_rowid");
|
|
|
+}
|
|
|
+
|
|
|
+void test_last_insert_rowid_in_transaction(Connection conn) throws SqlError {
|
|
|
+ print("Test: last_insert_rowid inside transaction... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE rowid_txn (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, label TEXT)");
|
|
|
+ var tx = conn.begin_transaction();
|
|
|
+ exec_p<string>(conn, "INSERT INTO rowid_txn (label) VALUES (:l)", "l", "in-txn");
|
|
|
+ int64 id_in_txn = conn.last_insert_rowid;
|
|
|
+ assert(id_in_txn > 0);
|
|
|
+ tx.commit();
|
|
|
+ // After commit the row keyed by the captured id is visible and matches.
|
|
|
+ var label = conn.create_command("SELECT label FROM rowid_txn WHERE id = :i")
|
|
|
+ .with_parameter<int64?>("i", id_in_txn).execute_scalar();
|
|
|
+ string? l = null;
|
|
|
+ assert(label.try_get_as<string>(out l) && l == "in-txn");
|
|
|
+ pass("last_insert_rowid in txn");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Lazy enumeration tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_lazy_enumeration_partial(Connection conn) throws SqlError {
|
|
|
+ print("Test: lazy enumeration, partial then abandon... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE lazy_rows (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, seq INT)");
|
|
|
+ var sets = new Invercargill.DataStructures.Vector<Invercargill.Properties>();
|
|
|
+ for (int i = 1; i <= 50; i++) {
|
|
|
+ var p = new Invercargill.DataStructures.PropertyDictionary();
|
|
|
+ p.set_native<int64?>("seq", i);
|
|
|
+ sets.add(p);
|
|
|
+ }
|
|
|
+ // Insert all 50 rows in one batch to have a known population.
|
|
|
+ int total = conn.create_command("INSERT INTO lazy_rows (seq) VALUES (:seq)")
|
|
|
+ .execute_batch(sets);
|
|
|
+ assert(total == 50);
|
|
|
+
|
|
|
+ // Enumerate only the first 3 rows, then abandon the enumerable.
|
|
|
+ var results = conn.create_command("SELECT id, seq FROM lazy_rows ORDER BY id").execute_query();
|
|
|
+ int seen = 0;
|
|
|
+ int64? last_seq = 0;
|
|
|
+ foreach (var walked_row in results) {
|
|
|
+ int64? seq_val = null;
|
|
|
+ assert(walked_row.get("seq").try_get_as<int64?>(out seq_val));
|
|
|
+ assert(seq_val > last_seq);
|
|
|
+ last_seq = seq_val;
|
|
|
+ seen++;
|
|
|
+ if (seen == 3) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assert(seen == 3);
|
|
|
+
|
|
|
+ // Abandoning mid-enumeration must not break subsequent queries on the
|
|
|
+ // same connection (result lifetime safety).
|
|
|
+ var n = conn.create_command("SELECT count(*) AS c FROM lazy_rows").execute_scalar();
|
|
|
+ int64? c = null;
|
|
|
+ assert(n.try_get_as<int64?>(out c) && c == 50);
|
|
|
+
|
|
|
+ // A fresh enumerable over the same data works fully.
|
|
|
+ int full = 0;
|
|
|
+ int64 seq_sum = 0;
|
|
|
+ foreach (var counted_row in conn.create_command("SELECT seq FROM lazy_rows").execute_query()) {
|
|
|
+ int64? seq_val = null;
|
|
|
+ assert(counted_row.get("seq").try_get_as<int64?>(out seq_val));
|
|
|
+ seq_sum += seq_val;
|
|
|
+ full++;
|
|
|
+ }
|
|
|
+ assert(full == 50);
|
|
|
+ assert(seq_sum == 1275); // 1+2+...+50
|
|
|
+ pass("lazy partial");
|
|
|
+}
|
|
|
+
|
|
|
+void test_peek_count(Connection conn) throws SqlError {
|
|
|
+ print("Test: peek_count reports buffered row count... ");
|
|
|
+ var empty = conn.create_command("SELECT 1 AS v WHERE false").execute_query();
|
|
|
+ assert(empty.peek_count() == 0);
|
|
|
+ var seven = conn.create_command("SELECT generate_series(1, 7) AS n").execute_query();
|
|
|
+ assert(seven.peek_count() == 7);
|
|
|
+ // peek_count is available without enumerating.
|
|
|
+ int walked = 0;
|
|
|
+ foreach (var series_row in seven) {
|
|
|
+ int64? n_val = null;
|
|
|
+ assert(series_row.get("n").try_get_as<int64?>(out n_val));
|
|
|
+ assert(n_val == walked + 1);
|
|
|
+ walked++;
|
|
|
+ }
|
|
|
+ assert(walked == 7);
|
|
|
+ pass("peek_count");
|
|
|
+}
|
|
|
+
|
|
|
+void test_enumerable_cache(Connection conn) throws SqlError {
|
|
|
+ print("Test: enumerable can be enumerated twice (cache)... ");
|
|
|
+ var rows = conn.create_command("SELECT generate_series(1, 5) AS n").execute_query();
|
|
|
+ int64 first_sum = 0;
|
|
|
+ foreach (var pass_row in rows) {
|
|
|
+ int64? n_val = null;
|
|
|
+ assert(pass_row.get("n").try_get_as<int64?>(out n_val));
|
|
|
+ first_sum += n_val;
|
|
|
+ }
|
|
|
+ int64 second_sum = 0;
|
|
|
+ foreach (var pass_row in rows) {
|
|
|
+ int64? n_val = null;
|
|
|
+ assert(pass_row.get("n").try_get_as<int64?>(out n_val));
|
|
|
+ second_sum += n_val;
|
|
|
+ }
|
|
|
+ assert(first_sum == 15 && second_sum == 15); // 1+2+3+4+5
|
|
|
+ pass("cache");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Error path tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_syntax_error(Connection conn) throws SqlError {
|
|
|
+ print("Test: syntax error surfaces as SqlError... ");
|
|
|
+ try {
|
|
|
+ conn.execute("SELEC nope FROM nothing");
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ // Server message must be carried into the error text.
|
|
|
+ assert(e.message.length > 0);
|
|
|
+ assert("SELEC" in e.message || "syntax" in e.message.down()
|
|
|
+ || e is SqlError.EXECUTION_FAILED || e is SqlError.TYPE_ERROR);
|
|
|
+ }
|
|
|
+ // Connection survives the error and remains usable.
|
|
|
+ var ok = conn.create_command("SELECT 'still-alive' AS s").execute_scalar();
|
|
|
+ string? s = null;
|
|
|
+ assert(ok.try_get_as<string>(out s) && s == "still-alive");
|
|
|
+ pass("syntax error");
|
|
|
+}
|
|
|
+
|
|
|
+void test_unique_violation(Connection conn) throws SqlError {
|
|
|
+ print("Test: unique violation surfaced... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE uniq_probe (id BIGINT PRIMARY KEY, email TEXT UNIQUE)");
|
|
|
+ conn.create_command("INSERT INTO uniq_probe (id, email) VALUES (:i, :e)")
|
|
|
+ .with_parameter<int64?>("i", 1).with_parameter("e", "a@x").execute_non_query();
|
|
|
+ try {
|
|
|
+ conn.create_command("INSERT INTO uniq_probe (id, email) VALUES (:i, :e)")
|
|
|
+ .with_parameter("i", 1).with_parameter("e", "a@x").execute_non_query();
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e.message.length > 0);
|
|
|
+ }
|
|
|
+ var n = conn.create_command("SELECT count(*) AS c FROM uniq_probe").execute_scalar();
|
|
|
+ int64? c = null;
|
|
|
+ assert(n.try_get_as<int64?>(out c) && c == 1);
|
|
|
+ pass("unique violation");
|
|
|
+}
|
|
|
+
|
|
|
+void test_missing_parameter(Connection conn) throws SqlError {
|
|
|
+ print("Test: missing parameter rejected... ");
|
|
|
+ try {
|
|
|
+ conn.create_command("SELECT :absent").execute_non_query();
|
|
|
+ assert_not_reached();
|
|
|
+ } catch (SqlError e) {
|
|
|
+ assert(e is SqlError.INVALID_PARAMETER);
|
|
|
+ assert(":absent" in e.message);
|
|
|
+ }
|
|
|
+ pass("missing parameter");
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Mixed integration tests
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+void test_mixed_type_table(Connection conn) throws SqlError {
|
|
|
+ print("Test: Mixed type table... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE mixed_pg (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, age INT, salary DOUBLE PRECISION, is_active BOOLEAN, created_at TIMESTAMPTZ, data BYTEA)");
|
|
|
+ var now = new DateTime.now_local();
|
|
|
+ uint8[] blob_bytes = { 0xDE, 0xAD, 0xBE, 0xEF };
|
|
|
+ double? salary_val = 75000.50;
|
|
|
+ conn.create_command("""
|
|
|
+ INSERT INTO mixed_pg (name, age, salary, is_active, created_at, data)
|
|
|
+ VALUES (:name, :age, :salary, :active, :created, :data)""")
|
|
|
+ .with_parameter("name", "John Doe")
|
|
|
+ .with_parameter("age", 30)
|
|
|
+ .with_parameter<double?>("salary", salary_val)
|
|
|
+ .with_parameter("active", true)
|
|
|
+ .with_parameter<DateTime>("created", now)
|
|
|
+ .with_parameter<Invercargill.BinaryData>("data", wrap_bytes(blob_bytes))
|
|
|
+ .execute_non_query();
|
|
|
+ var row = conn.create_command("SELECT * FROM mixed_pg").execute_query().to_array()[0];
|
|
|
+ string? name = null;
|
|
|
+ int? age = null;
|
|
|
+ double? salary = null;
|
|
|
+ bool? active = null;
|
|
|
+ DateTime? created = null;
|
|
|
+ assert(row.get("name").try_get_as<string>(out name) && name == "John Doe");
|
|
|
+ assert(row.get("age").try_get_as<int?>(out age) && age == 30);
|
|
|
+ assert(row.get("salary").try_get_as<double?>(out salary));
|
|
|
+ assert(salary >= 75000.49 && salary <= 75000.51);
|
|
|
+ assert(row.get("is_active").try_get_as<bool?>(out active) && active == true);
|
|
|
+ assert(row.get("created_at").try_get_as<DateTime>(out created));
|
|
|
+ assert(created != null && created.to_unix() == now.to_unix());
|
|
|
+ var data_elem = row.get("data");
|
|
|
+ assert(data_elem.is_null() == false);
|
|
|
+ pass("mixed table");
|
|
|
+}
|
|
|
+
|
|
|
+void test_element_assignable_types(Connection conn) throws SqlError {
|
|
|
+ print("Test: Element assignable_types over postgres types... ");
|
|
|
+ drop_and_create(conn, "CREATE TABLE assign_pg (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, int_val BIGINT, real_val DOUBLE PRECISION, str_val TEXT, bool_val BOOLEAN)");
|
|
|
+ conn.execute("INSERT INTO assign_pg (int_val, real_val, str_val, bool_val) VALUES (42, 3.14, 'test', true)");
|
|
|
+ var row = conn.create_command("SELECT int_val, real_val, str_val, bool_val FROM assign_pg")
|
|
|
+ .execute_query().to_array()[0];
|
|
|
+ var int_elem = row.get("int_val");
|
|
|
+ var real_elem = row.get("real_val");
|
|
|
+ var str_elem = row.get("str_val");
|
|
|
+ var bool_elem = row.get("bool_val");
|
|
|
+ assert(int_elem.assignable_to_type(typeof(int64)) == true);
|
|
|
+ assert(int_elem.assignable_to_type(typeof(int)) == true);
|
|
|
+ assert(int_elem.assignable_to_type(typeof(string)) == true);
|
|
|
+ assert(int_elem.assignable_to_type(typeof(double)) == true);
|
|
|
+ assert(real_elem.assignable_to_type(typeof(double)) == true);
|
|
|
+ assert(real_elem.assignable_to_type(typeof(float)) == true);
|
|
|
+ assert(real_elem.assignable_to_type(typeof(int64)) == true);
|
|
|
+ assert(real_elem.assignable_to_type(typeof(string)) == true);
|
|
|
+ assert(str_elem.assignable_to_type(typeof(string)) == true);
|
|
|
+ assert(str_elem.assignable_to_type(typeof(int64)) == true);
|
|
|
+ assert(str_elem.assignable_to_type(typeof(double)) == true);
|
|
|
+ assert(bool_elem.assignable_to_type(typeof(bool)) == true);
|
|
|
+ assert(bool_elem.assignable_to_type(typeof(string)) == true);
|
|
|
+ pass("assignable types");
|
|
|
+}
|