| 1 | #include "catch.hpp" |
| 2 | #include "test_helpers.hpp" |
| 3 | |
| 4 | #include <chrono> |
| 5 | #include <thread> |
| 6 | |
| 7 | using namespace duckdb; |
| 8 | using namespace std; |
| 9 | |
| 10 | TEST_CASE("Schema dependencies" , "[catalog]" ) { |
| 11 | unique_ptr<QueryResult> result; |
| 12 | DuckDB db(nullptr); |
| 13 | Connection con(db); |
| 14 | Connection con2(db); |
| 15 | |
| 16 | // single schema and dependencies |
| 17 | REQUIRE_NO_FAIL(con.Query("CREATE SCHEMA s1" )); |
| 18 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE s1.integers(i INTEGER)" )); |
| 19 | REQUIRE_NO_FAIL(con.Query("SELECT * FROM s1.integers" )); |
| 20 | // can't drop: dependency |
| 21 | REQUIRE_FAIL(con.Query("DROP SCHEMA s1" )); |
| 22 | REQUIRE_NO_FAIL(con.Query("SELECT * FROM s1.integers" )); |
| 23 | // we can drop with cascade though |
| 24 | REQUIRE_NO_FAIL(con.Query("DROP SCHEMA s1 CASCADE" )); |
| 25 | // this also drops the table |
| 26 | REQUIRE_FAIL(con.Query("SELECT * FROM s1.integers" )); |
| 27 | |
| 28 | // schemas and dependencies |
| 29 | // create a schema and a table inside the schema |
| 30 | REQUIRE_NO_FAIL(con.Query("CREATE SCHEMA s1" )); |
| 31 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE s1.integers(i INTEGER)" )); |
| 32 | |
| 33 | REQUIRE_NO_FAIL(con.Query("BEGIN TRANSACTION" )); |
| 34 | REQUIRE_NO_FAIL(con2.Query("BEGIN TRANSACTION" )); |
| 35 | // drop the table in con1 |
| 36 | REQUIRE_NO_FAIL(con.Query("DROP TABLE s1.integers" )); |
| 37 | // we can't drop the schema from con2 because the table still exists for con2! |
| 38 | REQUIRE_FAIL(con2.Query("DROP SCHEMA s1" )); |
| 39 | // now rollback the table drop |
| 40 | REQUIRE_NO_FAIL(con.Query("ROLLBACK" )); |
| 41 | REQUIRE_NO_FAIL(con2.Query("ROLLBACK" )); |
| 42 | // the table exists again |
| 43 | REQUIRE_NO_FAIL(con.Query("SELECT * FROM s1.integers" )); |
| 44 | // try again, but this time we commit |
| 45 | REQUIRE_NO_FAIL(con.Query("BEGIN TRANSACTION" )); |
| 46 | REQUIRE_NO_FAIL(con2.Query("BEGIN TRANSACTION" )); |
| 47 | // drop the schema entirely now |
| 48 | REQUIRE_NO_FAIL(con.Query("DROP SCHEMA s1 CASCADE" )); |
| 49 | // we can still query the table from con2 |
| 50 | REQUIRE_NO_FAIL(con2.Query("SELECT * FROM s1.integers" )); |
| 51 | // even after we commit |
| 52 | REQUIRE_NO_FAIL(con.Query("COMMIT" )); |
| 53 | REQUIRE_NO_FAIL(con2.Query("SELECT * FROM s1.integers" )); |
| 54 | // however if we end the transaction in con2 the schema is gone |
| 55 | REQUIRE_NO_FAIL(con2.Query("ROLLBACK" )); |
| 56 | REQUIRE_FAIL(con2.Query("CREATE TABLE s1.dummy(i INTEGER)" )); |
| 57 | } |
| 58 | |
| 59 | TEST_CASE("Prepared statement dependencies dependencies" , "[catalog]" ) { |
| 60 | unique_ptr<QueryResult> result; |
| 61 | DuckDB db(nullptr); |
| 62 | Connection con(db); |
| 63 | Connection con2(db); |
| 64 | |
| 65 | // prepared statements and dependencies |
| 66 | // dependency on a bound table |
| 67 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE integers(i INTEGER)" )); |
| 68 | REQUIRE_NO_FAIL(con2.Query("PREPARE v AS SELECT * FROM integers" )); |
| 69 | REQUIRE_NO_FAIL(con2.Query("EXECUTE v" )); |
| 70 | // cannot drop table now |
| 71 | REQUIRE_FAIL(con.Query("DROP TABLE integers" )); |
| 72 | // but CASCADE drop should work |
| 73 | REQUIRE_NO_FAIL(con.Query("DROP TABLE integers CASCADE" )); |
| 74 | // after the cascade drop the prepared statement is invalidated |
| 75 | REQUIRE_FAIL(con2.Query("EXECUTE v" )); |
| 76 | |
| 77 | // dependency on a sequence for prepare |
| 78 | REQUIRE_NO_FAIL(con.Query("CREATE SEQUENCE seq" )); |
| 79 | REQUIRE_NO_FAIL(con2.Query("PREPARE v AS SELECT nextval('seq')" )); |
| 80 | result = con2.Query("EXECUTE v" ); |
| 81 | REQUIRE(CHECK_COLUMN(result, 0, {1})); |
| 82 | // cannot drop sequence now |
| 83 | REQUIRE_FAIL(con.Query("DROP SEQUENCE seq" )); |
| 84 | // check that the prepared statement still works |
| 85 | result = con2.Query("EXECUTE v" ); |
| 86 | REQUIRE(CHECK_COLUMN(result, 0, {2})); |
| 87 | // cascade drop |
| 88 | REQUIRE_NO_FAIL(con.Query("DROP SEQUENCE seq CASCADE" )); |
| 89 | // after the cascade drop the prepared statement is invalidated |
| 90 | REQUIRE_FAIL(con2.Query("EXECUTE v" )); |
| 91 | } |
| 92 | |
| 93 | TEST_CASE("Default values and dependencies" , "[catalog]" ) { |
| 94 | unique_ptr<QueryResult> result; |
| 95 | DuckDB db(nullptr); |
| 96 | Connection con(db); |
| 97 | Connection con2(db); |
| 98 | |
| 99 | // dependency on a sequence in a default value |
| 100 | REQUIRE_NO_FAIL(con.Query("CREATE SEQUENCE seq" )); |
| 101 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE integers(i INTEGER DEFAULT nextval('seq'), j INTEGER)" )); |
| 102 | REQUIRE_NO_FAIL(con.Query("INSERT INTO integers (j) VALUES (1), (1), (1), (1), (1)" )); |
| 103 | |
| 104 | result = con2.Query("SELECT SUM(i) FROM integers" ); |
| 105 | REQUIRE(CHECK_COLUMN(result, 0, {15})); |
| 106 | // we can't drop the sequence: the table depends on it |
| 107 | REQUIRE_FAIL(con.Query("DROP SEQUENCE seq" )); |
| 108 | // cascade drop works |
| 109 | REQUIRE_NO_FAIL(con.Query("DROP SEQUENCE seq CASCADE" )); |
| 110 | // but it also drops the table |
| 111 | REQUIRE_FAIL(con.Query("SELECT * FROM integers" )); |
| 112 | |
| 113 | // dependency on multiple sequences in default value |
| 114 | REQUIRE_NO_FAIL(con.Query("CREATE SEQUENCE seq" )); |
| 115 | REQUIRE_NO_FAIL(con.Query("CREATE SEQUENCE seq1" )); |
| 116 | REQUIRE_NO_FAIL(con.Query("CREATE SEQUENCE seq2" )); |
| 117 | REQUIRE_NO_FAIL(con.Query( |
| 118 | "CREATE TABLE integers(i INTEGER DEFAULT nextval('seq' || CAST(nextval('seq') AS VARCHAR)), j INTEGER)" )); |
| 119 | |
| 120 | // seq1 exists, so the result of the first default value is 1 |
| 121 | REQUIRE_NO_FAIL(con.Query("INSERT INTO integers (j) VALUES (1)" )); |
| 122 | // we can drop seq1 and seq2: the dependency is not fixed |
| 123 | REQUIRE_NO_FAIL(con.Query("DROP SEQUENCE seq1" )); |
| 124 | REQUIRE_NO_FAIL(con.Query("DROP SEQUENCE seq2" )); |
| 125 | // seq2 does not exist after this drop, so another insert fails |
| 126 | REQUIRE_FAIL(con.Query("INSERT INTO integers (j) VALUES (1)" )); |
| 127 | // table is now [1, 1]: query it |
| 128 | result = con.Query("SELECT SUM(i) FROM integers" ); |
| 129 | REQUIRE(CHECK_COLUMN(result, 0, {1})); |
| 130 | // we can't drop seq however: the dependency is fixed |
| 131 | REQUIRE_FAIL(con.Query("DROP SEQUENCE seq" )); |
| 132 | // need to do a cascading drop |
| 133 | REQUIRE_NO_FAIL(con.Query("DROP SEQUENCE seq CASCADE" )); |
| 134 | // now the table is gone |
| 135 | REQUIRE_FAIL(con.Query("SELECT * FROM integers" )); |
| 136 | } |
| 137 | |
| 138 | TEST_CASE("Prepare dependencies and transactions" , "[catalog]" ) { |
| 139 | unique_ptr<QueryResult> result; |
| 140 | DuckDB db(nullptr); |
| 141 | Connection con(db); |
| 142 | Connection con2(db); |
| 143 | |
| 144 | // case one: prepared statement is created outside of transaction and committed |
| 145 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE integers(i INTEGER)" )); |
| 146 | REQUIRE_NO_FAIL(con.Query("INSERT INTO integers VALUES (1), (2), (3), (4), (5)" )); |
| 147 | REQUIRE_NO_FAIL(con2.Query("PREPARE v AS SELECT SUM(i) FROM integers" )); |
| 148 | |
| 149 | // begin a transaction in con2 |
| 150 | REQUIRE_NO_FAIL(con2.Query("BEGIN TRANSACTION" )); |
| 151 | // now drop the table in con, with a cascading drop |
| 152 | REQUIRE_NO_FAIL(con.Query("DROP TABLE integers CASCADE" )); |
| 153 | // we can still execute v in con2 |
| 154 | result = con2.Query("EXECUTE v" ); |
| 155 | REQUIRE(CHECK_COLUMN(result, 0, {15})); |
| 156 | // if we try to drop integers we get a conflict though |
| 157 | REQUIRE_FAIL(con2.Query("DROP TABLE integers CASCADE" )); |
| 158 | // now we rollback |
| 159 | REQUIRE_NO_FAIL(con2.Query("ROLLBACK" )); |
| 160 | // now we can't use the prepared statement anymore |
| 161 | REQUIRE_FAIL(con2.Query("EXECUTE v" )); |
| 162 | |
| 163 | // case two: prepared statement is created inside transaction |
| 164 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE integers(i INTEGER)" )); |
| 165 | REQUIRE_NO_FAIL(con.Query("INSERT INTO integers VALUES (1), (2), (3), (4), (5)" )); |
| 166 | |
| 167 | // begin a transaction and create a prepared statement |
| 168 | REQUIRE_NO_FAIL(con2.Query("BEGIN TRANSACTION" )); |
| 169 | REQUIRE_NO_FAIL(con2.Query("PREPARE v AS SELECT SUM(i) FROM integers" )); |
| 170 | |
| 171 | // integers has a dependency: we can't drop it |
| 172 | REQUIRE_FAIL(con.Query("DROP TABLE integers" )); |
| 173 | // now we can't drop integers even with cascade, because the dependency is not yet committed, this creates a write |
| 174 | // conflict on attempting to drop the dependency |
| 175 | REQUIRE_FAIL(con.Query("DROP TABLE integers CASCADE" )); |
| 176 | |
| 177 | // use the prepared statement |
| 178 | result = con2.Query("EXECUTE v" ); |
| 179 | REQUIRE(CHECK_COLUMN(result, 0, {15})); |
| 180 | // and commit |
| 181 | REQUIRE_NO_FAIL(con2.Query("COMMIT" )); |
| 182 | |
| 183 | // now we can commit the table |
| 184 | REQUIRE_NO_FAIL(con.Query("DROP TABLE integers CASCADE" )); |
| 185 | |
| 186 | // case three: prepared statement is created inside transaction, and then rolled back |
| 187 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE integers(i INTEGER)" )); |
| 188 | REQUIRE_NO_FAIL(con.Query("INSERT INTO integers VALUES (1), (2), (3), (4), (5)" )); |
| 189 | |
| 190 | // begin a transaction and create a prepared statement |
| 191 | REQUIRE_NO_FAIL(con2.Query("BEGIN TRANSACTION" )); |
| 192 | REQUIRE_NO_FAIL(con2.Query("PREPARE v AS SELECT SUM(i) FROM integers" )); |
| 193 | // integers has a dependency: we can't drop it |
| 194 | REQUIRE_FAIL(con.Query("DROP TABLE integers" )); |
| 195 | // rollback the prepared statement |
| 196 | REQUIRE_NO_FAIL(con2.Query("ROLLBACK" )); |
| 197 | // depedency was rolled back: now we can drop it |
| 198 | REQUIRE_NO_FAIL(con.Query("DROP TABLE integers" )); |
| 199 | |
| 200 | // case four: deallocate happens inside transaction |
| 201 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE integers(i INTEGER)" )); |
| 202 | REQUIRE_NO_FAIL(con.Query("INSERT INTO integers VALUES (1), (2), (3), (4), (5)" )); |
| 203 | REQUIRE_NO_FAIL(con2.Query("PREPARE v AS SELECT SUM(i) FROM integers" )); |
| 204 | |
| 205 | // deallocate v inside transaction |
| 206 | REQUIRE_NO_FAIL(con2.Query("BEGIN TRANSACTION" )); |
| 207 | REQUIRE_NO_FAIL(con2.Query("DEALLOCATE v" )); |
| 208 | |
| 209 | // we still can't drop integers because the dependency is still there |
| 210 | REQUIRE_FAIL(con.Query("DROP TABLE integers" )); |
| 211 | // cascade gives a concurrency conflict |
| 212 | REQUIRE_FAIL(con.Query("DROP TABLE integers CASCADE" )); |
| 213 | // now rollback the deallocation |
| 214 | REQUIRE_NO_FAIL(con2.Query("ROLLBACK" )); |
| 215 | // still can't drop the table |
| 216 | REQUIRE_FAIL(con.Query("DROP TABLE integers" )); |
| 217 | // we can use the prepared statement again |
| 218 | result = con2.Query("EXECUTE v" ); |
| 219 | REQUIRE(CHECK_COLUMN(result, 0, {15})); |
| 220 | // now do the same as before, but commit the transaction this time |
| 221 | REQUIRE_NO_FAIL(con2.Query("BEGIN TRANSACTION" )); |
| 222 | REQUIRE_NO_FAIL(con2.Query("DEALLOCATE v" )); |
| 223 | // can't drop yet: not yet committed |
| 224 | REQUIRE_FAIL(con.Query("DROP TABLE integers" )); |
| 225 | REQUIRE_FAIL(con.Query("DROP TABLE integers CASCADE" )); |
| 226 | REQUIRE_NO_FAIL(con2.Query("COMMIT" )); |
| 227 | // after committing we can drop |
| 228 | REQUIRE_NO_FAIL(con.Query("DROP TABLE integers" )); |
| 229 | } |
| 230 | |
| 231 | TEST_CASE("Test prepare dependencies with multiple connections" , "[catalog]" ) { |
| 232 | unique_ptr<QueryResult> result; |
| 233 | DuckDB db(nullptr); |
| 234 | auto con = make_unique<Connection>(db); |
| 235 | auto con2 = make_unique<Connection>(db); |
| 236 | auto con3 = make_unique<Connection>(db); |
| 237 | |
| 238 | // simple prepare: begin transaction before the second client calls PREPARE |
| 239 | REQUIRE_NO_FAIL(con->Query("CREATE TABLE integers(i INTEGER)" )); |
| 240 | // open a transaction in con2, this forces the prepared statement to be kept around until this transaction is closed |
| 241 | REQUIRE_NO_FAIL(con2->Query("BEGIN TRANSACTION" )); |
| 242 | // we prepare a statement in con |
| 243 | REQUIRE_NO_FAIL(con->Query("PREPARE s1 AS SELECT * FROM integers" )); |
| 244 | // now we drop con while the second client still has an active transaction |
| 245 | con.reset(); |
| 246 | // now commit the transaction in the second client |
| 247 | REQUIRE_NO_FAIL(con2->Query("COMMIT" )); |
| 248 | |
| 249 | con = make_unique<Connection>(db); |
| 250 | // three transactions |
| 251 | // open a transaction in con2, this forces the prepared statement to be kept around until this transaction is closed |
| 252 | REQUIRE_NO_FAIL(con2->Query("BEGIN TRANSACTION" )); |
| 253 | // create a prepare, this creates a dependency from s1 -> integers |
| 254 | REQUIRE_NO_FAIL(con->Query("PREPARE s1 AS SELECT * FROM integers" )); |
| 255 | // drop the client |
| 256 | con.reset(); |
| 257 | // now begin a transaction in con3 |
| 258 | REQUIRE_NO_FAIL(con3->Query("BEGIN TRANSACTION" )); |
| 259 | // drop the table integers with cascade, this should drop s1 as well |
| 260 | REQUIRE_NO_FAIL(con3->Query("DROP TABLE integers CASCADE" )); |
| 261 | REQUIRE_NO_FAIL(con2->Query("COMMIT" )); |
| 262 | REQUIRE_NO_FAIL(con3->Query("COMMIT" )); |
| 263 | } |
| 264 | |
| 265 | #define REPETITIONS 100 |
| 266 | #define SEQTHREADS 10 |
| 267 | volatile bool finished = false; |
| 268 | |
| 269 | static void RunQueryUntilSuccess(Connection &con, string query) { |
| 270 | while (true) { |
| 271 | auto result = con.Query(query); |
| 272 | if (result->success) { |
| 273 | break; |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | static void create_drop_table(DuckDB *db) { |
| 279 | Connection con(*db); |
| 280 | |
| 281 | while (!finished) { |
| 282 | // printf("[TABLE] Create table\n"); |
| 283 | // create the table: this should never fail |
| 284 | REQUIRE_NO_FAIL(con.Query("BEGIN TRANSACTION" )); |
| 285 | REQUIRE_NO_FAIL(con.Query("CREATE TABLE integers(i INTEGER)" )); |
| 286 | REQUIRE_NO_FAIL(con.Query("INSERT INTO integers VALUES (1), (2), (3), (4), (5)" )); |
| 287 | REQUIRE_NO_FAIL(con.Query("COMMIT" )); |
| 288 | // now wait a bit |
| 289 | this_thread::sleep_for(chrono::milliseconds(20)); |
| 290 | // printf("[TABLE] Drop table\n"); |
| 291 | // perform a cascade drop of the table |
| 292 | // this can fail if a thread is still busy preparing a statement |
| 293 | RunQueryUntilSuccess(con, "DROP TABLE integers CASCADE" ); |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | static void create_use_prepared_statement(DuckDB *db) { |
| 298 | Connection con(*db); |
| 299 | unique_ptr<QueryResult> result; |
| 300 | |
| 301 | for (int i = 0; i < REPETITIONS; i++) { |
| 302 | // printf("[PREPARE] Prepare statement\n"); |
| 303 | RunQueryUntilSuccess(con, "PREPARE s1 AS SELECT SUM(i) FROM integers" ); |
| 304 | // printf("[PREPARE] Query prepare\n"); |
| 305 | while (true) { |
| 306 | // execute the prepared statement until the prepared statement is dropped because of the CASCADE in another |
| 307 | // thread |
| 308 | result = con.Query("EXECUTE s1" ); |
| 309 | if (!result->success) { |
| 310 | break; |
| 311 | } else { |
| 312 | REQUIRE(CHECK_COLUMN(result, 0, {15})); |
| 313 | } |
| 314 | } |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | TEST_CASE("Test parallel dependencies in multiple connections" , "[catalog][.]" ) { |
| 319 | DuckDB db(nullptr); |
| 320 | |
| 321 | // in this test we create and drop a table in one thread (with CASCADE drop) |
| 322 | // in the other thread, we create a prepared statement and execute it |
| 323 | // the prepared statement depends on the table |
| 324 | // hence when the CASCADE drop is executed the prepared statement also needs to be dropped |
| 325 | |
| 326 | thread table_thread = thread(create_drop_table, &db); |
| 327 | thread seq_threads[SEQTHREADS]; |
| 328 | for (int i = 0; i < SEQTHREADS; i++) { |
| 329 | seq_threads[i] = thread(create_use_prepared_statement, &db); |
| 330 | } |
| 331 | for (int i = 0; i < SEQTHREADS; i++) { |
| 332 | seq_threads[i].join(); |
| 333 | } |
| 334 | finished = true; |
| 335 | table_thread.join(); |
| 336 | } |
| 337 | |
| 338 | static void create_drop_schema(DuckDB *db) { |
| 339 | Connection con(*db); |
| 340 | |
| 341 | while (!finished) { |
| 342 | // create the schema: this should never fail |
| 343 | REQUIRE_NO_FAIL(con.Query("CREATE SCHEMA s1" )); |
| 344 | // now wait a bit |
| 345 | this_thread::sleep_for(chrono::milliseconds(20)); |
| 346 | // perform a cascade drop of the schema |
| 347 | // this can fail if a thread is still busy creating something inside the schema |
| 348 | RunQueryUntilSuccess(con, "DROP SCHEMA s1 CASCADE" ); |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | static void create_use_table_view(DuckDB *db, int threadnr) { |
| 353 | Connection con(*db); |
| 354 | unique_ptr<QueryResult> result; |
| 355 | string tname = "integers" + to_string(threadnr); |
| 356 | string vname = "v" + to_string(threadnr); |
| 357 | |
| 358 | for (int i = 0; i < REPETITIONS; i++) { |
| 359 | RunQueryUntilSuccess(con, "CREATE TABLE s1." + tname + "(i INTEGER)" ); |
| 360 | con.Query("INSERT INTO s1." + tname + " VALUES (1), (2), (3), (4), (5)" ); |
| 361 | RunQueryUntilSuccess(con, "CREATE VIEW s1." + vname + " AS SELECT 42" ); |
| 362 | while (true) { |
| 363 | result = con.Query("SELECT SUM(i) FROM s1." + tname); |
| 364 | if (!result->success) { |
| 365 | break; |
| 366 | } else { |
| 367 | REQUIRE(CHECK_COLUMN(result, 0, {15})); |
| 368 | } |
| 369 | result = con.Query("SELECT * FROM s1." + vname); |
| 370 | if (!result->success) { |
| 371 | break; |
| 372 | } else { |
| 373 | REQUIRE(CHECK_COLUMN(result, 0, {42})); |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | } |
| 378 | TEST_CASE("Test parallel dependencies with schemas and tables" , "[catalog][.]" ) { |
| 379 | DuckDB db(nullptr); |
| 380 | // FIXME: this test crashes |
| 381 | return; |
| 382 | |
| 383 | // in this test we create and drop a schema in one thread (with CASCADE drop) |
| 384 | // in other threads, we create tables and views and query those tables and views |
| 385 | |
| 386 | thread table_thread = thread(create_drop_schema, &db); |
| 387 | thread seq_threads[SEQTHREADS]; |
| 388 | for (int i = 0; i < SEQTHREADS; i++) { |
| 389 | seq_threads[i] = thread(create_use_table_view, &db, i); |
| 390 | } |
| 391 | for (int i = 0; i < SEQTHREADS; i++) { |
| 392 | seq_threads[i].join(); |
| 393 | } |
| 394 | finished = true; |
| 395 | table_thread.join(); |
| 396 | } |
| 397 | |