@@ -803,25 +803,124 @@ bool database_check_schema_hash (cloudsync_context *data, uint64_t hash) {
803803}
804804
805805int database_update_schema_hash (cloudsync_context * data , uint64_t * hash ) {
806- char * schemasql = "SELECT group_concat(LOWER(sql)) FROM sqlite_master "
807- "WHERE type = 'table' AND name IN (SELECT tbl_name FROM cloudsync_table_settings ORDER BY tbl_name) "
808- "ORDER BY name;" ;
809-
806+ // Build normalized schema string using only: column name (lowercase), type (SQLite affinity), pk flag
807+ // Format: tablename:colname:affinity:pk,... (ordered by table name, then column id)
808+ // This makes the hash resilient to formatting, quoting, case differences and portable across databases
809+ //
810+ // Type mapping (simplified from SQLite affinity rules for cross-database compatibility):
811+ // - Types containing 'INT' → 'integer'
812+ // - Types containing 'CHAR', 'CLOB', 'TEXT' → 'text'
813+ // - Types containing 'BLOB' or empty → 'blob'
814+ // - Types containing 'REAL', 'FLOA', 'DOUB' → 'real'
815+ // - Types exactly 'NUMERIC' or 'DECIMAL' → 'numeric'
816+ // - Everything else → 'text' (default)
817+ //
818+ // NOTE: This deviates from SQLite's actual affinity rules where unknown types get NUMERIC affinity.
819+ // We use 'text' as default to improve cross-database compatibility with PostgreSQL, where types
820+ // like TIMESTAMPTZ, UUID, JSON, etc. are commonly used and map to 'text' in the PostgreSQL
821+ // implementation. This ensures schemas with PostgreSQL-specific type names in SQLite DDL
822+ // will hash consistently across both databases.
823+ sqlite3 * db = (sqlite3 * )cloudsync_db (data );
824+
825+ char * * tables = NULL ;
826+ int ntables , tcols ;
827+ int rc = sqlite3_get_table (db , "SELECT DISTINCT tbl_name FROM cloudsync_table_settings ORDER BY tbl_name;" ,
828+ & tables , & ntables , & tcols , NULL );
829+ if (rc != SQLITE_OK || ntables == 0 ) {
830+ if (tables ) sqlite3_free_table (tables );
831+ return SQLITE_ERROR ;
832+ }
833+
810834 char * schema = NULL ;
811- int rc = database_select_text (data , schemasql , & schema );
812- if (rc != DBRES_OK ) return rc ;
813- if (!schema ) return DBRES_ERROR ;
814-
815- uint64_t h = fnv1a_hash (schema , strlen (schema ));
835+ size_t schema_len = 0 ;
836+ size_t schema_cap = 0 ;
837+
838+ for (int t = 1 ; t <= ntables ; t ++ ) {
839+ const char * tbl_name = tables [t ];
840+
841+ // Query pragma_table_info for this table with normalized type
842+ char * col_sql = cloudsync_memory_mprintf (
843+ "SELECT LOWER(name), "
844+ "CASE "
845+ " WHEN UPPER(type) LIKE '%%INT%%' THEN 'integer' "
846+ " WHEN UPPER(type) LIKE '%%CHAR%%' OR UPPER(type) LIKE '%%CLOB%%' OR UPPER(type) LIKE '%%TEXT%%' THEN 'text' "
847+ " WHEN UPPER(type) LIKE '%%BLOB%%' OR type = '' THEN 'blob' "
848+ " WHEN UPPER(type) LIKE '%%REAL%%' OR UPPER(type) LIKE '%%FLOA%%' OR UPPER(type) LIKE '%%DOUB%%' THEN 'real' "
849+ " WHEN UPPER(type) IN ('NUMERIC', 'DECIMAL') THEN 'numeric' "
850+ " ELSE 'text' "
851+ "END, "
852+ "CASE WHEN pk > 0 THEN '1' ELSE '0' END "
853+ "FROM pragma_table_info('%q') ORDER BY cid;" , tbl_name );
854+
855+ if (!col_sql ) {
856+ if (schema ) cloudsync_memory_free (schema );
857+ sqlite3_free_table (tables );
858+ return SQLITE_NOMEM ;
859+ }
860+
861+ char * * cols = NULL ;
862+ int nrows , ncols ;
863+ rc = sqlite3_get_table (db , col_sql , & cols , & nrows , & ncols , NULL );
864+ cloudsync_memory_free (col_sql );
865+
866+ if (rc != SQLITE_OK || ncols != 3 ) {
867+ if (cols ) sqlite3_free_table (cols );
868+ if (schema ) cloudsync_memory_free (schema );
869+ sqlite3_free_table (tables );
870+ return SQLITE_ERROR ;
871+ }
872+
873+ // Append each column: tablename:colname:affinity:pk
874+ for (int r = 1 ; r <= nrows ; r ++ ) {
875+ const char * col_name = cols [r * 3 ];
876+ const char * col_type = cols [r * 3 + 1 ];
877+ const char * col_pk = cols [r * 3 + 2 ];
878+
879+ // Calculate required size: tbl_name:col_name:col_type:col_pk,
880+ size_t entry_len = strlen (tbl_name ) + 1 + strlen (col_name ) + 1 + strlen (col_type ) + 1 + strlen (col_pk ) + 1 ;
881+
882+ if (schema_len + entry_len + 1 > schema_cap ) {
883+ schema_cap = (schema_cap == 0 ) ? 1024 : schema_cap * 2 ;
884+ if (schema_cap < schema_len + entry_len + 1 ) schema_cap = schema_len + entry_len + 1 ;
885+ char * new_schema = cloudsync_memory_realloc (schema , schema_cap );
886+ if (!new_schema ) {
887+ if (schema ) cloudsync_memory_free (schema );
888+ sqlite3_free_table (cols );
889+ sqlite3_free_table (tables );
890+ return SQLITE_NOMEM ;
891+ }
892+ schema = new_schema ;
893+ }
894+
895+ int written = snprintf (schema + schema_len , schema_cap - schema_len , "%s:%s:%s:%s," ,
896+ tbl_name , col_name , col_type , col_pk );
897+ schema_len += written ;
898+ }
899+
900+ sqlite3_free_table (cols );
901+ }
902+
903+ sqlite3_free_table (tables );
904+
905+ if (!schema || schema_len == 0 ) return SQLITE_ERROR ;
906+
907+ // Remove trailing comma
908+ if (schema_len > 0 && schema [schema_len - 1 ] == ',' ) {
909+ schema [schema_len - 1 ] = '\0' ;
910+ schema_len -- ;
911+ }
912+
913+ DEBUG_MERGE ("database_update_schema_hash len %zu schema %s" , schema_len , schema );
914+ sqlite3_uint64 h = fnv1a_hash (schema , schema_len );
816915 cloudsync_memory_free (schema );
817916 if (hash && * hash == h ) return SQLITE_CONSTRAINT ;
818-
917+
819918 char sql [1024 ];
820919 snprintf (sql , sizeof (sql ), "INSERT INTO cloudsync_schema_versions (hash, seq) "
821- "VALUES (%" PRIu64 " , COALESCE((SELECT MAX(seq) FROM cloudsync_schema_versions), 0) + 1) "
920+ "VALUES (%lld , COALESCE((SELECT MAX(seq) FROM cloudsync_schema_versions), 0) + 1) "
822921 "ON CONFLICT(hash) DO UPDATE SET "
823- "seq = (SELECT COALESCE(MAX(seq), 0) + 1 FROM cloudsync_schema_versions);" , h );
824- rc = database_exec ( data , sql );
922+ " seq = (SELECT COALESCE(MAX(seq), 0) + 1 FROM cloudsync_schema_versions);" , ( sqlite3_int64 ) h );
923+ rc = sqlite3_exec ( db , sql , NULL , NULL , NULL );
825924 if (rc == SQLITE_OK && hash ) * hash = h ;
826925 return rc ;
827926}
0 commit comments