Skip to content

Commit c7dd77d

Browse files
committed
Fix for PG UUID used as PK
1 parent 8512b26 commit c7dd77d

File tree

5 files changed

+249
-15
lines changed

5 files changed

+249
-15
lines changed

src/cloudsync.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
extern "C" {
1818
#endif
1919

20-
#define CLOUDSYNC_VERSION "0.9.97"
20+
#define CLOUDSYNC_VERSION "0.9.98"
2121
#define CLOUDSYNC_MAX_TABLENAME_LEN 512
2222

2323
#define CLOUDSYNC_VALUE_NOTSET -1

src/postgresql/database_postgresql.c

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,7 +1695,7 @@ int database_pk_names (cloudsync_context *data, const char *table_name, char ***
16951695
int rc = SPI_execute_with_args(sql, 1, argtypes, values, nulls, true, 0);
16961696
pfree(DatumGetPointer(values[0]));
16971697

1698-
if (rc < 0 || SPI_processed == 0) {
1698+
if (rc != SPI_OK_SELECT || SPI_processed == 0) {
16991699
*names = NULL;
17001700
*count = 0;
17011701
if (SPI_tuptable) SPI_freetuptable(SPI_tuptable);
@@ -1704,22 +1704,25 @@ int database_pk_names (cloudsync_context *data, const char *table_name, char ***
17041704

17051705
uint64_t n = SPI_processed;
17061706
char **pk_names = cloudsync_memory_zeroalloc(n * sizeof(char*));
1707-
if (!pk_names) return DBRES_NOMEM;
1707+
if (!pk_names) {
1708+
if (SPI_tuptable) SPI_freetuptable(SPI_tuptable);
1709+
return DBRES_NOMEM;
1710+
}
17081711

17091712
for (uint64_t i = 0; i < n; i++) {
17101713
HeapTuple tuple = SPI_tuptable->vals[i];
17111714
bool isnull;
17121715
Datum datum = SPI_getbinval(tuple, SPI_tuptable->tupdesc, 1, &isnull);
17131716
if (!isnull) {
1714-
text *txt = DatumGetTextP(datum);
1715-
char *name = text_to_cstring(txt);
1717+
// information_schema.column_name is of type 'name', not 'text'
1718+
Name namedata = DatumGetName(datum);
1719+
char *name = (namedata) ? NameStr(*namedata) : NULL;
17161720
pk_names[i] = (name) ? cloudsync_string_dup(name) : NULL;
1717-
if (name) pfree(name);
17181721
}
17191722

17201723
// Cleanup on allocation failure
17211724
if (!isnull && pk_names[i] == NULL) {
1722-
for (int j = 0; j < i; j++) {
1725+
for (uint64_t j = 0; j < i; j++) {
17231726
if (pk_names[j]) cloudsync_memory_free(pk_names[j]);
17241727
}
17251728
cloudsync_memory_free(pk_names);

src/postgresql/sql_postgresql.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ const char * const SQL_BUILD_DELETE_ROW_BY_PK =
172172
" SELECT to_regclass('%s') AS oid"
173173
"), "
174174
"pk AS ("
175-
" SELECT a.attname, k.ord "
175+
" SELECT a.attname, k.ord, format_type(a.atttypid, a.atttypmod) AS coltype "
176176
" FROM pg_index x "
177177
" JOIN tbl t ON t.oid = x.indrelid "
178178
" JOIN LATERAL unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord) ON true "
@@ -183,7 +183,7 @@ const char * const SQL_BUILD_DELETE_ROW_BY_PK =
183183
"SELECT "
184184
" 'DELETE FROM ' || (SELECT (oid::regclass)::text FROM tbl)"
185185
" || ' WHERE '"
186-
" || (SELECT string_agg(format('%%I=$%%s', attname, ord), ' AND ' ORDER BY ord) FROM pk)"
186+
" || (SELECT string_agg(format('%%I=$%%s::%%s', attname, ord, coltype), ' AND ' ORDER BY ord) FROM pk)"
187187
" || ';';";
188188

189189
const char * const SQL_INSERT_ROWID_IGNORE =
@@ -198,7 +198,7 @@ const char * const SQL_BUILD_INSERT_PK_IGNORE =
198198
" SELECT to_regclass('%s') AS oid"
199199
"), "
200200
"pk AS ("
201-
" SELECT a.attname, k.ord "
201+
" SELECT a.attname, k.ord, format_type(a.atttypid, a.atttypmod) AS coltype "
202202
" FROM pg_index x "
203203
" JOIN tbl t ON t.oid = x.indrelid "
204204
" JOIN LATERAL unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord) ON true "
@@ -209,15 +209,15 @@ const char * const SQL_BUILD_INSERT_PK_IGNORE =
209209
"SELECT "
210210
" 'INSERT INTO ' || (SELECT (oid::regclass)::text FROM tbl)"
211211
" || ' (' || (SELECT string_agg(format('%%I', attname), ',') FROM pk) || ')'"
212-
" || ' VALUES (' || (SELECT string_agg(format('$%%s', ord), ',') FROM pk) || ')'"
212+
" || ' VALUES (' || (SELECT string_agg(format('$%%s::%%s', ord, coltype), ',') FROM pk) || ')'"
213213
" || ' ON CONFLICT DO NOTHING;';";
214214

215215
const char * const SQL_BUILD_UPSERT_PK_AND_COL =
216216
"WITH tbl AS ("
217217
" SELECT to_regclass('%s') AS oid"
218218
"), "
219219
"pk AS ("
220-
" SELECT a.attname, k.ord "
220+
" SELECT a.attname, k.ord, format_type(a.atttypid, a.atttypmod) AS coltype "
221221
" FROM pg_index x "
222222
" JOIN tbl t ON t.oid = x.indrelid "
223223
" JOIN LATERAL unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord) ON true "
@@ -235,7 +235,7 @@ const char * const SQL_BUILD_UPSERT_PK_AND_COL =
235235
" 'INSERT INTO ' || (SELECT (oid::regclass)::text FROM tbl)"
236236
" || ' (' || (SELECT string_agg(format('%%I', attname), ',') FROM pk)"
237237
" || ',' || (SELECT format('%%I', colname) FROM col) || ')'"
238-
" || ' VALUES (' || (SELECT string_agg(format('$%%s', ord), ',') FROM pk)"
238+
" || ' VALUES (' || (SELECT string_agg(format('$%%s::%%s', ord, coltype), ',') FROM pk)"
239239
" || ',' || (SELECT format('$%%s', (SELECT n FROM pk_count) + 1)) || ')'"
240240
" || ' ON CONFLICT (' || (SELECT string_agg(format('%%I', attname), ',') FROM pk) || ')'"
241241
" || ' DO UPDATE SET ' || (SELECT format('%%I', colname) FROM col)"
@@ -249,7 +249,7 @@ const char * const SQL_BUILD_SELECT_COLS_BY_PK_FMT =
249249
" SELECT to_regclass('%s') AS tblreg"
250250
"), "
251251
"pk AS ("
252-
" SELECT a.attname, k.ord "
252+
" SELECT a.attname, k.ord, format_type(a.atttypid, a.atttypmod) AS coltype "
253253
" FROM pg_index x "
254254
" JOIN tbl t ON t.tblreg = x.indrelid "
255255
" JOIN LATERAL unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord) ON true "
@@ -264,7 +264,7 @@ const char * const SQL_BUILD_SELECT_COLS_BY_PK_FMT =
264264
" 'SELECT ' || (SELECT format('%%I', colname) FROM col) "
265265
" || ' FROM ' || (SELECT tblreg::text FROM tbl)"
266266
" || ' WHERE '"
267-
" || (SELECT string_agg(format('%%I=$%%s', attname, ord), ' AND ' ORDER BY ord) FROM pk)"
267+
" || (SELECT string_agg(format('%%I=$%%s::%%s', attname, ord, coltype), ' AND ' ORDER BY ord) FROM pk)"
268268
" || ';';";
269269

270270
const char * const SQL_CLOUDSYNC_ROW_EXISTS_BY_PK =
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
-- UUID Primary Key Roundtrip Test
2+
-- Tests roundtrip with a UUID primary key (single column).
3+
4+
\set testid '17'
5+
\ir helper_test_init.sql
6+
7+
\connect postgres
8+
\ir helper_psql_conn_setup.sql
9+
10+
-- Cleanup and create test databases
11+
DROP DATABASE IF EXISTS cloudsync_test_17a;
12+
DROP DATABASE IF EXISTS cloudsync_test_17b;
13+
CREATE DATABASE cloudsync_test_17a;
14+
CREATE DATABASE cloudsync_test_17b;
15+
16+
-- ============================================================================
17+
-- Setup Database A with UUID primary key
18+
-- ============================================================================
19+
20+
\connect cloudsync_test_17a
21+
\ir helper_psql_conn_setup.sql
22+
CREATE EXTENSION IF NOT EXISTS cloudsync;
23+
24+
CREATE TABLE products (
25+
id UUID PRIMARY KEY,
26+
name TEXT NOT NULL DEFAULT '',
27+
price DOUBLE PRECISION NOT NULL DEFAULT 0.0,
28+
stock INTEGER NOT NULL DEFAULT 0,
29+
metadata BYTEA
30+
);
31+
32+
-- Initialize CloudSync
33+
SELECT cloudsync_init('products', 'CLS', false) AS _init_a \gset
34+
35+
-- ============================================================================
36+
-- Insert test data with UUIDs
37+
-- ============================================================================
38+
39+
INSERT INTO products VALUES ('550e8400-e29b-41d4-a716-446655440000', 'Product A', 99.99, 100, E'\\xDEADBEEF');
40+
INSERT INTO products VALUES ('6ba7b810-9dad-11d1-80b4-00c04fd430c8', 'Product B', 49.50, 50, NULL);
41+
INSERT INTO products VALUES ('6ba7b811-9dad-11d1-80b4-00c04fd430c8', 'Product C', 0.0, 0, E'\\x00');
42+
INSERT INTO products VALUES ('6ba7b812-9dad-11d1-80b4-00c04fd430c8', 'Product D', 123.45, 999, E'\\xCAFEBABE');
43+
INSERT INTO products VALUES ('6ba7b813-9dad-11d1-80b4-00c04fd430c8', '', -1.0, -1, E'\\x010203');
44+
45+
-- ============================================================================
46+
-- Compute hash of Database A data
47+
-- ============================================================================
48+
49+
SELECT md5(
50+
COALESCE(
51+
string_agg(
52+
id::text || ':' ||
53+
COALESCE(name, 'NULL') || ':' ||
54+
COALESCE(price::text, 'NULL') || ':' ||
55+
COALESCE(stock::text, 'NULL') || ':' ||
56+
COALESCE(encode(metadata, 'hex'), 'NULL'),
57+
'|' ORDER BY id
58+
),
59+
''
60+
)
61+
) AS hash_a FROM products \gset
62+
63+
\echo [INFO] (:testid) Database A hash: :hash_a
64+
65+
-- ============================================================================
66+
-- Encode payload from Database A
67+
-- ============================================================================
68+
69+
SELECT encode(
70+
cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq),
71+
'hex'
72+
) AS payload_a_hex
73+
FROM cloudsync_changes
74+
WHERE site_id = cloudsync_siteid() \gset
75+
76+
-- Verify payload was created
77+
SELECT (length(:'payload_a_hex') > 0) AS payload_created \gset
78+
\if :payload_created
79+
\echo [PASS] (:testid) Payload encoded from Database A
80+
\else
81+
\echo [FAIL] (:testid) Payload encoded from Database A - Empty payload
82+
SELECT (:fail::int + 1) AS fail \gset
83+
\endif
84+
85+
-- ============================================================================
86+
-- Setup Database B with same schema
87+
-- ============================================================================
88+
89+
\connect cloudsync_test_17b
90+
\ir helper_psql_conn_setup.sql
91+
CREATE EXTENSION IF NOT EXISTS cloudsync;
92+
93+
CREATE TABLE products (
94+
id UUID PRIMARY KEY,
95+
name TEXT NOT NULL DEFAULT '',
96+
price DOUBLE PRECISION NOT NULL DEFAULT 0.0,
97+
stock INTEGER NOT NULL DEFAULT 0,
98+
metadata BYTEA
99+
);
100+
101+
-- Initialize CloudSync
102+
SELECT cloudsync_init('products', 'CLS', false) AS _init_b \gset
103+
104+
-- ============================================================================
105+
-- Apply payload to Database B
106+
-- ============================================================================
107+
108+
SELECT cloudsync_payload_apply(decode(:'payload_a_hex', 'hex')) AS apply_result \gset
109+
110+
-- Verify application succeeded
111+
SELECT (:apply_result >= 0) AS payload_applied \gset
112+
\if :payload_applied
113+
\echo [PASS] (:testid) Payload applied to Database B
114+
\else
115+
\echo [FAIL] (:testid) Payload applied to Database B - Apply returned :apply_result
116+
SELECT (:fail::int + 1) AS fail \gset
117+
\endif
118+
119+
-- ============================================================================
120+
-- Verify data integrity after roundtrip
121+
-- ============================================================================
122+
123+
SELECT md5(
124+
COALESCE(
125+
string_agg(
126+
id::text || ':' ||
127+
COALESCE(name, 'NULL') || ':' ||
128+
COALESCE(price::text, 'NULL') || ':' ||
129+
COALESCE(stock::text, 'NULL') || ':' ||
130+
COALESCE(encode(metadata, 'hex'), 'NULL'),
131+
'|' ORDER BY id
132+
),
133+
''
134+
)
135+
) AS hash_b FROM products \gset
136+
137+
\echo [INFO] (:testid) Database B hash: :hash_b
138+
139+
SELECT (:'hash_a' = :'hash_b') AS hashes_match \gset
140+
\if :hashes_match
141+
\echo [PASS] (:testid) Data integrity verified - hashes match
142+
\else
143+
\echo [FAIL] (:testid) Data integrity check failed - Database A hash: :hash_a, Database B hash: :hash_b
144+
SELECT (:fail::int + 1) AS fail \gset
145+
\endif
146+
147+
-- ============================================================================
148+
-- Verify row count
149+
-- ============================================================================
150+
151+
SELECT COUNT(*) AS count_b FROM products \gset
152+
\connect cloudsync_test_17a
153+
SELECT COUNT(*) AS count_a_orig FROM products \gset
154+
155+
\connect cloudsync_test_17b
156+
SELECT (:count_b = :count_a_orig) AS row_counts_match \gset
157+
\if :row_counts_match
158+
\echo [PASS] (:testid) Row counts match (:count_b rows)
159+
\else
160+
\echo [FAIL] (:testid) Row counts mismatch - Database A: :count_a_orig, Database B: :count_b
161+
SELECT (:fail::int + 1) AS fail \gset
162+
\endif
163+
164+
-- ============================================================================
165+
-- Verify UUID primary keys preserved
166+
-- ============================================================================
167+
168+
SELECT COUNT(DISTINCT id) = 5 AS uuid_count_ok FROM products \gset
169+
\if :uuid_count_ok
170+
\echo [PASS] (:testid) UUID primary keys preserved
171+
\else
172+
\echo [FAIL] (:testid) UUID primary keys not all preserved
173+
SELECT (:fail::int + 1) AS fail \gset
174+
\endif
175+
176+
-- ============================================================================
177+
-- Test specific UUID values
178+
-- ============================================================================
179+
180+
SELECT COUNT(*) = 1 AS uuid_test_ok
181+
FROM products
182+
WHERE id = '550e8400-e29b-41d4-a716-446655440000'
183+
AND name = 'Product A'
184+
AND price = 99.99 \gset
185+
\if :uuid_test_ok
186+
\echo [PASS] (:testid) Specific UUID record verified
187+
\else
188+
\echo [FAIL] (:testid) Specific UUID record not found or incorrect
189+
SELECT (:fail::int + 1) AS fail \gset
190+
\endif
191+
192+
-- ============================================================================
193+
-- Test bidirectional sync (B -> A)
194+
-- ============================================================================
195+
196+
\connect cloudsync_test_17b
197+
198+
INSERT INTO products VALUES ('7ba7b814-9dad-11d1-80b4-00c04fd430c8', 'From B', 77.77, 777, E'\\xBEEF');
199+
200+
SELECT encode(
201+
cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq),
202+
'hex'
203+
) AS payload_b_hex
204+
FROM cloudsync_changes
205+
WHERE site_id = cloudsync_siteid() \gset
206+
207+
\connect cloudsync_test_17a
208+
SELECT cloudsync_payload_apply(decode(:'payload_b_hex', 'hex')) AS apply_b_to_a \gset
209+
210+
SELECT COUNT(*) = 1 AS bidirectional_ok
211+
FROM products
212+
WHERE id = '7ba7b814-9dad-11d1-80b4-00c04fd430c8'
213+
AND name = 'From B'
214+
AND price = 77.77 \gset
215+
\if :bidirectional_ok
216+
\echo [PASS] (:testid) Bidirectional sync works (B to A)
217+
\else
218+
\echo [FAIL] (:testid) Bidirectional sync failed
219+
SELECT (:fail::int + 1) AS fail \gset
220+
\endif
221+
222+
-- ============================================================================
223+
-- Cleanup: Drop test databases if not in DEBUG mode and no failures
224+
-- ============================================================================
225+
226+
\ir helper_test_cleanup.sql
227+
\if :should_cleanup
228+
DROP DATABASE IF EXISTS cloudsync_test_17a;
229+
DROP DATABASE IF EXISTS cloudsync_test_17b;
230+
\endif

test/postgresql/full_test.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
\ir 14_datatype_roundtrip.sql
2525
\ir 15_datatype_roundtrip_unmapped.sql
2626
\ir 16_composite_pk_text_int_roundtrip.sql
27+
\ir 17_uuid_pk_roundtrip.sql
2728

2829
-- 'Test summary'
2930
\echo '\nTest summary:'

0 commit comments

Comments
 (0)