Skip to content

Commit 9b6e28f

Browse files
committed
Added a new compilation macro CLOUDSYNC_CHECK_NOTNULL_PRIKEYS (disabled by default)
1 parent cb582c1 commit 9b6e28f

File tree

6 files changed

+49
-24
lines changed

6 files changed

+49
-24
lines changed

src/cloudsync.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2678,6 +2678,7 @@ int cloudsync_table_sanity_check (cloudsync_context *data, const char *name, boo
26782678
}
26792679

26802680
// if user declared explicit primary key(s) then make sure they are all declared as NOT NULL
2681+
#if CLOUDSYNC_CHECK_NOTNULL_PRIKEYS
26812682
if (npri_keys > 0) {
26822683
int npri_keys_notnull = database_count_pk(data, name, true, cloudsync_schema(data));
26832684
if (npri_keys_notnull < 0) return cloudsync_set_dberror(data);
@@ -2686,6 +2687,7 @@ int cloudsync_table_sanity_check (cloudsync_context *data, const char *name, boo
26862687
return cloudsync_set_error(data, buffer, DBRES_ERROR);
26872688
}
26882689
}
2690+
#endif
26892691

26902692
// check for columns declared as NOT NULL without a DEFAULT value.
26912693
// Otherwise, col_merge_stmt would fail if changes to other columns are inserted first.

src/pk.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
#define DATABASE_TYPE_MAX_NEGATIVE_INTEGER 6 // was SQLITE_MAX_NEGATIVE_INTEGER
8888
#define DATABASE_TYPE_NEGATIVE_FLOAT 7 // was SQLITE_NEGATIVE_FLOAT
8989

90+
char * const PRIKEY_NULL_CONSTRAINT_ERROR = "PRIKEY_NULL_CONSTRAINT_ERROR";
91+
9092
// MARK: - Public Callbacks -
9193

9294
int pk_decode_bind_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
@@ -436,7 +438,14 @@ char *pk_encode (dbvalue_t **argv, int argc, char *b, bool is_prikey, size_t *bs
436438
if (!bsize) return NULL;
437439
// must fit in a single byte
438440
if (argc > 255) return NULL;
439-
441+
442+
// if schema does not enforce NOT NULL on primary keys, check at runtime
443+
#ifndef CLOUDSYNC_CHECK_NOTNULL_PRIKEYS
444+
for (int i = 0; i < argc; i++) {
445+
if (database_value_type(argv[i]) == DBTYPE_NULL) return PRIKEY_NULL_CONSTRAINT_ERROR;
446+
}
447+
#endif
448+
440449
// 1 is the number of items in the serialization
441450
// always 1 byte so max 255 primary keys, even if there is an hard SQLite limit of 128
442451
size_t blen_curr = *bsize;

src/pk.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
typedef int (*pk_decode_callback) (void *xdata, int index, int type, int64_t ival, double dval, char *pval);
1717

18+
extern char * const PRIKEY_NULL_CONSTRAINT_ERROR;
19+
1820
char *pk_encode_prikey (dbvalue_t **argv, int argc, char *b, size_t *bsize);
1921
char *pk_encode_value (dbvalue_t *value, size_t *bsize);
2022
char *pk_encode (dbvalue_t **argv, int argc, char *b, bool is_prikey, size_t *bsize, int skip_idx);

src/postgresql/cloudsync_postgresql.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ Datum cloudsync_pk_encode (PG_FUNCTION_ARGS) {
11221122

11231123
size_t pklen = 0;
11241124
char *encoded = pk_encode_prikey((dbvalue_t **)argv, argc, NULL, &pklen);
1125-
if (!encoded) {
1125+
if (!encoded || encoded == PRIKEY_NULL_CONSTRAINT_ERROR) {
11261126
ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("cloudsync_pk_encode failed to encode primary key")));
11271127
}
11281128

@@ -1271,6 +1271,10 @@ Datum cloudsync_insert (PG_FUNCTION_ARGS) {
12711271
if (!cleanup.pk) {
12721272
ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("Not enough memory to encode the primary key(s)")));
12731273
}
1274+
if (cleanup.pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
1275+
cleanup.pk = NULL;
1276+
ereport(ERROR, (errcode(ERRCODE_NOT_NULL_VIOLATION), errmsg("Insert aborted because primary key in table %s contains NULL values", table_name)));
1277+
}
12741278

12751279
// Compute the next database version for tracking changes
12761280
int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
@@ -1360,6 +1364,10 @@ Datum cloudsync_delete (PG_FUNCTION_ARGS) {
13601364
if (!cleanup.pk) {
13611365
ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("Not enough memory to encode the primary key(s)")));
13621366
}
1367+
if (cleanup.pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
1368+
cleanup.pk = NULL;
1369+
ereport(ERROR, (errcode(ERRCODE_NOT_NULL_VIOLATION), errmsg("Delete aborted because primary key in table %s contains NULL values", table_name)));
1370+
}
13631371

13641372
int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
13651373

@@ -1561,6 +1569,10 @@ Datum cloudsync_update_finalfn (PG_FUNCTION_ARGS) {
15611569
if (!pk) {
15621570
ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("Not enough memory to encode the primary key(s)")));
15631571
}
1572+
if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
1573+
pk = NULL;
1574+
ereport(ERROR, (errcode(ERRCODE_NOT_NULL_VIOLATION), errmsg("Update aborted because primary key in table %s contains NULL values", table_name)));
1575+
}
15641576
if (prikey_changed) {
15651577
oldpk = pk_encode_prikey((dbvalue_t **)payload->old_values, pk_count, buffer2, &oldpklen);
15661578
if (!oldpk) {

src/sqlite/cloudsync_sqlite.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ void dbsync_col_value (sqlite3_context *context, int argc, sqlite3_value **argv)
260260
void dbsync_pk_encode (sqlite3_context *context, int argc, sqlite3_value **argv) {
261261
size_t bsize = 0;
262262
char *buffer = pk_encode_prikey((dbvalue_t **)argv, argc, NULL, &bsize);
263-
if (!buffer) {
263+
if (!buffer || buffer == PRIKEY_NULL_CONSTRAINT_ERROR) {
264264
sqlite3_result_null(context);
265265
return;
266266
}
@@ -347,6 +347,10 @@ void dbsync_insert (sqlite3_context *context, int argc, sqlite3_value **argv) {
347347
sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
348348
return;
349349
}
350+
if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
351+
dbsync_set_error(context, "Insert aborted because primary key in table %s contains NULL values.", table_name);
352+
return;
353+
}
350354

351355
// compute the next database version for tracking changes
352356
int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
@@ -407,6 +411,11 @@ void dbsync_delete (sqlite3_context *context, int argc, sqlite3_value **argv) {
407411
return;
408412
}
409413

414+
if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
415+
dbsync_set_error(context, "Delete aborted because primary key in table %s contains NULL values.", table_name);
416+
return;
417+
}
418+
410419
// mark the row as deleted by inserting a delete sentinel into the metadata
411420
rc = local_mark_delete_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
412421
if (rc != SQLITE_OK) goto cleanup;
@@ -542,6 +551,11 @@ void dbsync_update_final (sqlite3_context *context) {
542551
dbsync_update_payload_free(payload);
543552
return;
544553
}
554+
if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
555+
dbsync_set_error(context, "Update aborted because primary key in table %s contains NULL values.", table_name);
556+
dbsync_update_payload_free(payload);
557+
return;
558+
}
545559

546560
if (prikey_changed) {
547561
// if the primary key has changed, we need to handle the row differently:
@@ -551,6 +565,7 @@ void dbsync_update_final (sqlite3_context *context) {
551565
// encode the OLD primary key into a buffer
552566
oldpk = pk_encode_prikey((dbvalue_t **)payload->old_values, table_count_pks(table), buffer2, &oldpklen);
553567
if (!oldpk) {
568+
// no check here about PRIKEY_NULL_CONSTRAINT_ERROR because by design oldpk cannot contain NULL values
554569
if (pk != buffer) cloudsync_memory_free(pk);
555570
sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
556571
dbsync_update_payload_free(payload);

test/unit.c

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,7 +1716,7 @@ bool do_test_pk (sqlite3 *db, int ntest, bool print_result) {
17161716
if (do_test_pk_single_value(db, SQLITE_INTEGER, -15592946911031981, 0, NULL, print_result) == false) goto finalize;
17171717
if (do_test_pk_single_value(db, SQLITE_INTEGER, -922337203685477580, 0, NULL, print_result) == false) goto finalize;
17181718
if (do_test_pk_single_value(db, SQLITE_FLOAT, 0, -9223372036854775.808, NULL, print_result) == false) goto finalize;
1719-
if (do_test_pk_single_value(db, SQLITE_NULL, 0, 0, NULL, print_result) == false) goto finalize;
1719+
// SQLITE_NULL is no longer valid for primary keys (runtime NULL check rejects it)
17201720
if (do_test_pk_single_value(db, SQLITE_TEXT, 0, 0, "Hello World", print_result) == false) goto finalize;
17211721
char blob[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
17221722
if (do_test_pk_single_value(db, SQLITE_BLOB, sizeof(blob), 0, blob, print_result) == false) goto finalize;
@@ -2381,8 +2381,8 @@ bool do_test_pk_decode_count_from_buffer(void) {
23812381
rc = sqlite3_cloudsync_init(db, NULL, NULL);
23822382
if (rc != SQLITE_OK) goto cleanup;
23832383

2384-
// Encode multiple values
2385-
const char *sql = "SELECT cloudsync_pk_encode(123, 'text value', 3.14, X'DEADBEEF', NULL);";
2384+
// Encode multiple values (no NULL — primary keys cannot contain NULL)
2385+
const char *sql = "SELECT cloudsync_pk_encode(123, 'text value', 3.14, X'DEADBEEF');";
23862386
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
23872387
if (rc != SQLITE_OK) goto cleanup;
23882388

@@ -2403,7 +2403,7 @@ bool do_test_pk_decode_count_from_buffer(void) {
24032403
// The count is embedded in the first byte of the encoded pk
24042404
size_t seek = 0;
24052405
int n = pk_decode(buffer, (size_t)pklen, -1, &seek, -1, NULL, NULL);
2406-
if (n != 5) goto cleanup; // Should decode 5 values
2406+
if (n != 4) goto cleanup; // Should decode 4 values
24072407

24082408
result = true;
24092409

@@ -2849,8 +2849,8 @@ bool do_test_sql_pk_decode(void) {
28492849
rc = sqlite3_cloudsync_init(db, NULL, NULL);
28502850
if (rc != SQLITE_OK) goto cleanup;
28512851

2852-
// Create a primary key with multiple values
2853-
rc = sqlite3_prepare_v2(db, "SELECT cloudsync_pk_encode(123, 'hello', 3.14, X'DEADBEEF', NULL);", -1, &stmt, NULL);
2852+
// Create a primary key with multiple values (no NULL — primary keys cannot contain NULL)
2853+
rc = sqlite3_prepare_v2(db, "SELECT cloudsync_pk_encode(123, 'hello', 3.14, X'DEADBEEF');", -1, &stmt, NULL);
28542854
if (rc != SQLITE_OK) goto cleanup;
28552855

28562856
rc = sqlite3_step(stmt);
@@ -2934,21 +2934,6 @@ bool do_test_sql_pk_decode(void) {
29342934
sqlite3_finalize(stmt);
29352935
stmt = NULL;
29362936

2937-
// Test cloudsync_pk_decode for NULL (index 5)
2938-
rc = sqlite3_prepare_v2(db, "SELECT cloudsync_pk_decode(?, 5);", -1, &stmt, NULL);
2939-
if (rc != SQLITE_OK) goto cleanup;
2940-
2941-
rc = sqlite3_bind_blob(stmt, 1, pk_copy, pk_len, SQLITE_STATIC);
2942-
if (rc != SQLITE_OK) goto cleanup;
2943-
2944-
rc = sqlite3_step(stmt);
2945-
if (rc != SQLITE_ROW) goto cleanup;
2946-
2947-
if (sqlite3_column_type(stmt, 0) != SQLITE_NULL) goto cleanup;
2948-
2949-
sqlite3_finalize(stmt);
2950-
stmt = NULL;
2951-
29522937
result = true;
29532938

29542939
cleanup:

0 commit comments

Comments
 (0)