Pharos 0.7.23
Modern Web Framework & Web App Hosting Service.
Loading...
Searching...
No Matches
native_migrate_verify.c
Go to the documentation of this file.
1/**
2 * @file
3 * @brief Verifies sqlite migration state natively and emits structured migration verification reports.
4 */
5
7
8#ifndef _WIN32
9
11#include "native_json_config.h"
13#include "native_support.h"
15
16#include <stdio.h>
17#include <stdlib.h>
18#include <string.h>
19#include <sys/wait.h>
20#include <unistd.h>
21
22#define native_migrate_verify_strdup strdup
23
24/**
25 * @brief Duplicates a static JSON literal string.
26 * @param value JSON literal text to duplicate.
27 * @return Newly allocated copy, or NULL on allocation failure.
28 */
29static char *native_json_literal_dup(const char *value) {
30 return native_migrate_verify_strdup(value != NULL ? value : "null");
31}
32
33/**
34 * @brief Duplicates a string value as JSON string text, or null when absent.
35 * @param value Source string.
36 * @return Newly allocated JSON string payload or null literal.
37 */
38static char *native_json_nullable_string_dup(const char *value) {
39 if (value == NULL || value[0] == '\0') {
40 return native_json_literal_dup("null");
41 }
42 return json_string_dup_native(value);
43}
44
45/**
46 * @brief Duplicates a borrowed JSON value as compact JSON, or a fallback literal.
47 * @param value Borrowed JSON value.
48 * @param fallback Fallback JSON literal.
49 * @return Newly allocated JSON text.
50 */
52 const char *fallback) {
53 char *json = native_json_serialize_compact_dup(value);
54 if (json != NULL) {
55 return json;
56 }
57 return native_json_literal_dup(fallback);
58}
59
60/**
61 * @brief Duplicates an object string field as JSON string text with a default.
62 * @param obj Object to inspect.
63 * @param key Field name.
64 * @param fallback Default plain string value.
65 * @return Newly allocated JSON string text.
66 */
68 const char *key,
69 const char *fallback) {
70 char *text = native_json_obj_get_string_dup(obj, key);
71 char *json = NULL;
72 if (text == NULL) {
73 return json_string_dup_native(fallback != NULL ? fallback : "");
74 }
75 json = json_string_dup_native(text);
76 free(text);
77 return json;
78}
79
80/**
81 * @brief Converts a long value to owned JSON number text.
82 * @param value Numeric value to serialize.
83 * @return Newly allocated JSON number text, or NULL on allocation failure.
84 */
85static char *native_long_json_dup(long value) {
86 char buffer[64];
87 snprintf(buffer, sizeof(buffer), "%ld", value);
88 return native_migrate_verify_strdup(buffer);
89}
90
91/**
92 * @brief Tests whether a string is a safe sqlite identifier.
93 * @param value Candidate identifier.
94 * @return 1 when the identifier is safe, otherwise 0.
95 */
96static int native_identifier_safe(const char *value) {
97 const char *cursor = value;
98 if (value == NULL || value[0] == '\0') {
99 return 0;
100 }
101 while (*cursor != '\0') {
102 if (!((*cursor >= 'A' && *cursor <= 'Z') ||
103 (*cursor >= 'a' && *cursor <= 'z') ||
104 (*cursor >= '0' && *cursor <= '9') || *cursor == '_')) {
105 return 0;
106 }
107 cursor++;
108 }
109 return 1;
110}
111
112/**
113 * @brief Resolves an executable from PATH.
114 * @param command Command name or path.
115 * @return Newly allocated executable path, or NULL when unavailable.
116 */
117static char *native_resolve_command_path(const char *command) {
118 char *path_copy;
119 char *segment;
120 char *state = NULL;
121 const char *path_env;
122 if (command == NULL || command[0] == '\0') {
123 return NULL;
124 }
125 if (strchr(command, '/') != NULL) {
126 return access(command, X_OK) == 0
128 : NULL;
129 }
130 path_env = getenv("PATH");
131 if (path_env == NULL || path_env[0] == '\0') {
132 return NULL;
133 }
134 path_copy = native_migrate_verify_strdup(path_env);
135 if (path_copy == NULL) {
136 return NULL;
137 }
138 for (segment = strtok_r(path_copy, ":", &state); segment != NULL;
139 segment = strtok_r(NULL, ":", &state)) {
140 char candidate[4096];
141 if (segment[0] == '\0') {
142 continue;
143 }
144 snprintf(candidate, sizeof(candidate), "%s/%s", segment, command);
145 if (access(candidate, X_OK) == 0) {
146 char *resolved = native_migrate_verify_strdup(candidate);
147 free(path_copy);
148 return resolved;
149 }
150 }
151 free(path_copy);
152 return NULL;
153}
154
155/**
156 * @brief Tests whether a required native tool is available.
157 * @param command Command to resolve.
158 * @return 1 when present, otherwise 0.
159 */
160static int native_command_exists(const char *command) {
161 char *resolved = native_resolve_command_path(command);
162 int present = resolved != NULL && resolved[0] != '\0';
163 free(resolved);
164 return present;
165}
166
167/**
168 * @brief Runs a subprocess and trims surrounding whitespace from stdout.
169 * @param argv Command argv vector.
170 * @param code_out Optional process exit code output.
171 * @return Newly allocated trimmed stdout text, or NULL on process failure.
172 */
173static char *native_capture_trimmed(char *const argv[], int *code_out) {
174 char *output = NULL;
175 int code = run_process_capture_native(argv, &output);
176 if (output != NULL) {
177 trim_in_place(output);
178 }
179 if (code_out != NULL) {
180 *code_out = code;
181 }
182 return output;
183}
184
185/**
186 * @brief Appends one SQL character with sqlite literal escaping.
187 * @param buffer Destination buffer.
188 * @param value Character to append.
189 * @return 0 on success, otherwise -1.
190 */
191static int native_append_escaped_sql_char(Buffer *buffer, char value) {
192 if (buffer == NULL) {
193 return -1;
194 }
195 switch (value) {
196 case '\'':
197 return buffer_append(buffer, "''", 2);
198 default:
199 return buffer_append(buffer, &value, 1);
200 }
201}
202
203/**
204 * @brief Escapes a string for safe inclusion in a sqlite string literal.
205 * @param value Input string.
206 * @return Newly allocated escaped text, or NULL on allocation failure.
207 */
208static char *native_sql_literal_escape(const char *value) {
209 Buffer buffer;
210 const char *cursor = value != NULL ? value : "";
211 buffer_init(&buffer);
212 while (*cursor != '\0') {
213 if (native_append_escaped_sql_char(&buffer, *cursor) != 0) {
214 buffer_free(&buffer);
215 return NULL;
216 }
217 cursor++;
218 }
219 return buffer.data;
220}
221
222/**
223 * @brief Writes temporary text content to a file.
224 * @param text Text payload.
225 * @param path_out Receives the created path.
226 * @return 0 on success, otherwise -1.
227 */
228static int native_write_temp_text_file(const char *text, char **path_out) {
229 char path_template[] = "/tmp/pharos-migrate-XXXXXX";
230 FILE *file = NULL;
231 int fd;
232 size_t text_len = text != NULL ? strlen(text) : 0;
233 if (path_out == NULL) {
234 return -1;
235 }
236 *path_out = NULL;
237 fd = mkstemp(path_template);
238 if (fd < 0) {
239 return -1;
240 }
241 file = fdopen(fd, "wb");
242 if (file == NULL) {
243 close(fd);
244 remove(path_template);
245 return -1;
246 }
247 if (text_len > 0 && fwrite(text, 1, text_len, file) != text_len) {
248 fclose(file);
249 remove(path_template);
250 return -1;
251 }
252 if (fclose(file) != 0) {
253 remove(path_template);
254 return -1;
255 }
256 *path_out = native_migrate_verify_strdup(path_template);
257 if (*path_out == NULL) {
258 remove(path_template);
259 return -1;
260 }
261 return 0;
262}
263
264/**
265 * @brief Captures checksum tool output for a temporary file.
266 * @param command Checksum command.
267 * @param arg1 Optional first flag.
268 * @param arg2 Optional second flag.
269 * @param file_path File to checksum.
270 * @param code_out Optional exit code output.
271 * @return Newly allocated trimmed stdout, or NULL.
272 */
273static char *native_capture_checksum_output(const char *command,
274 const char *arg1,
275 const char *arg2,
276 const char *file_path,
277 int *code_out) {
278 char *resolved_path = NULL;
279 char *argv_list[5];
280 int argc = 0;
281 char *output;
282 if (command == NULL || file_path == NULL) {
283 return NULL;
284 }
285 resolved_path = native_resolve_command_path(command);
286 if (resolved_path == NULL) {
287 return NULL;
288 }
289 argv_list[argc++] = resolved_path;
290 if (arg1 != NULL) {
291 argv_list[argc++] = (char *)arg1;
292 }
293 if (arg2 != NULL) {
294 argv_list[argc++] = (char *)arg2;
295 }
296 argv_list[argc++] = (char *)file_path;
297 argv_list[argc] = NULL;
298 output = native_capture_trimmed(argv_list, code_out);
299 free(resolved_path);
300 return output;
301}
302
303/**
304 * @brief Evaluates whether a seed expectation passes.
305 * @param row_count Observed row count.
306 * @param minimum_present Whether minimum_rows was provided.
307 * @param minimum_rows Minimum row threshold.
308 * @param exact_present Whether exact_rows was provided.
309 * @param exact_rows Exact row requirement.
310 * @param reason_out Receives the failure reason.
311 * @return 1 when satisfied, otherwise 0.
312 */
313static int native_seed_expectation_satisfied(long row_count,
314 int minimum_present,
315 long minimum_rows,
316 int exact_present,
317 long exact_rows,
318 const char **reason_out) {
319 if (reason_out == NULL) {
320 return 0;
321 }
322 if (exact_present && row_count != exact_rows) {
323 *reason_out = "exact_rows_mismatch";
324 return 0;
325 }
326 if (minimum_present && row_count < minimum_rows) {
327 *reason_out = "minimum_rows_not_met";
328 return 0;
329 }
330 *reason_out = "ok";
331 return 1;
332}
333
334/**
335 * @brief Tests whether a JSON string array contains one exact string.
336 * @param array_value Array to inspect.
337 * @param expected_value Expected string.
338 * @return 1 when found, otherwise 0.
339 */
341 const char *expected_value) {
342 yyjson_arr_iter iter;
343 yyjson_val *item = NULL;
344 if (array_value == NULL || expected_value == NULL ||
345 !yyjson_is_arr(array_value)) {
346 return 0;
347 }
348 if (!yyjson_arr_iter_init(array_value, &iter)) {
349 return 0;
350 }
351 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
352 if (yyjson_is_str(item) && yyjson_equals_str(item, expected_value)) {
353 return 1;
354 }
355 }
356 return 0;
357}
358
359/**
360 * @brief Tests whether an applied checksum satisfies expected checksum contracts.
361 * @param applied Whether the migration is applied.
362 * @param applied_checksum Applied checksum text.
363 * @param expected_checksum Primary expected checksum.
364 * @param legacy_expected_checksum Legacy expected checksum.
365 * @param accepted_legacy_checksums Array of accepted legacy checksums.
366 * @return 1 when the checksum matches, otherwise 0.
367 */
369 int applied,
370 const char *applied_checksum,
371 const char *expected_checksum,
372 const char *legacy_expected_checksum,
373 yyjson_val *accepted_legacy_checksums) {
374 if (!applied || applied_checksum == NULL || applied_checksum[0] == '\0') {
375 return 0;
376 }
377 if (expected_checksum != NULL && streq(applied_checksum, expected_checksum)) {
378 return 1;
379 }
380 if (legacy_expected_checksum != NULL &&
381 streq(applied_checksum, legacy_expected_checksum)) {
382 return 1;
383 }
384 return native_json_string_array_contains(accepted_legacy_checksums,
385 applied_checksum);
386}
387
388/**
389 * @brief Executes a sqlite query and captures one trimmed stdout value.
390 * @param db_path Database path.
391 * @param sql SQL statement.
392 * @return Newly allocated stdout text, or NULL on failure.
393 */
394static char *native_sqlite_query_value(const char *db_path, const char *sql) {
395 char *argv_list[4];
396 char *sqlite_path = NULL;
397 char *output = NULL;
398 int code = 0;
399 if (db_path == NULL || sql == NULL) {
400 return NULL;
401 }
402 sqlite_path = native_resolve_command_path("sqlite3");
403 if (sqlite_path == NULL) {
404 return NULL;
405 }
406 argv_list[0] = sqlite_path;
407 argv_list[1] = (char *)db_path;
408 argv_list[2] = (char *)sql;
409 argv_list[3] = NULL;
410 output = native_capture_trimmed(argv_list, &code);
411 free(sqlite_path);
412 if (code != 0) {
413 free(output);
414 return NULL;
415 }
416 return output;
417}
418
419/**
420 * @brief Tests whether a sqlite table exists.
421 * @param db_path Database path.
422 * @param table_name Table name.
423 * @return 1 when present, otherwise 0.
424 */
425static int native_sqlite_table_exists(const char *db_path,
426 const char *table_name) {
427 char query[1024];
428 char *escaped = NULL;
429 char *result = NULL;
430 int exists = 0;
431 if (db_path == NULL || !native_identifier_safe(table_name)) {
432 return 0;
433 }
434 escaped = native_sql_literal_escape(table_name);
435 if (escaped == NULL) {
436 return 0;
437 }
438 snprintf(query, sizeof(query),
439 "SELECT name FROM sqlite_master WHERE type = 'table' AND name = "
440 "'%s' LIMIT 1;",
441 escaped);
442 result = native_sqlite_query_value(db_path, query);
443 if (result != NULL && streq(result, table_name)) {
444 exists = 1;
445 }
446 free(escaped);
447 free(result);
448 return exists;
449}
450
451/**
452 * @brief Counts rows in a sqlite table.
453 * @param db_path Database path.
454 * @param table_name Table name.
455 * @return Observed row count or 0 on failure.
456 */
457static long native_sqlite_table_row_count(const char *db_path,
458 const char *table_name) {
459 char query[1024];
460 char *result = NULL;
461 long value = 0;
462 if (db_path == NULL || !native_identifier_safe(table_name)) {
463 return 0;
464 }
465 snprintf(query, sizeof(query), "SELECT COUNT(*) FROM \"%s\";", table_name);
466 result = native_sqlite_query_value(db_path, query);
467 if (result != NULL && result[0] != '\0') {
468 value = strtol(result, NULL, 10);
469 }
470 free(result);
471 return value;
472}
473
474/**
475 * @brief Counts applied migration ledger rows.
476 * @param db_path Database path.
477 * @param migration_id Migration id.
478 * @param backend Backend name.
479 * @return Count of matching applied rows.
480 */
481static long native_sqlite_migration_applied_count(const char *db_path,
482 const char *migration_id,
483 const char *backend) {
484 char query[2048];
485 char *escaped_id = NULL;
486 char *escaped_backend = NULL;
487 char *result = NULL;
488 long value = 0;
489 if (db_path == NULL || migration_id == NULL || backend == NULL) {
490 return 0;
491 }
492 escaped_id = native_sql_literal_escape(migration_id);
493 escaped_backend = native_sql_literal_escape(backend);
494 if (escaped_id == NULL || escaped_backend == NULL) {
495 free(escaped_id);
496 free(escaped_backend);
497 return 0;
498 }
499 snprintf(query, sizeof(query),
500 "SELECT COUNT(*) FROM pharos_schema_migration WHERE migration_id = "
501 "'%s' AND backend = '%s';",
502 escaped_id, escaped_backend);
503 result = native_sqlite_query_value(db_path, query);
504 if (result != NULL && result[0] != '\0') {
505 value = strtol(result, NULL, 10);
506 }
507 free(escaped_id);
508 free(escaped_backend);
509 free(result);
510 return value;
511}
512
513/**
514 * @brief Reads the applied schema version for a migration.
515 * @param db_path Database path.
516 * @param migration_id Migration id.
517 * @param backend Backend name.
518 * @return Newly allocated schema version text, or NULL on failure.
519 */
521 const char *db_path,
522 const char *migration_id,
523 const char *backend) {
524 char query[2048];
525 char *escaped_id = NULL;
526 char *escaped_backend = NULL;
527 char *result = NULL;
528 if (db_path == NULL || migration_id == NULL || backend == NULL) {
529 return NULL;
530 }
531 escaped_id = native_sql_literal_escape(migration_id);
532 escaped_backend = native_sql_literal_escape(backend);
533 if (escaped_id == NULL || escaped_backend == NULL) {
534 free(escaped_id);
535 free(escaped_backend);
536 return NULL;
537 }
538 snprintf(query, sizeof(query),
539 "SELECT schema_version FROM pharos_schema_migration WHERE "
540 "migration_id = '%s' AND backend = '%s' ORDER BY applied_at DESC "
541 "LIMIT 1;",
542 escaped_id, escaped_backend);
543 result = native_sqlite_query_value(db_path, query);
544 free(escaped_id);
545 free(escaped_backend);
546 return result;
547}
548
549/**
550 * @brief Reads the applied checksum for a migration.
551 * @param db_path Database path.
552 * @param migration_id Migration id.
553 * @param backend Backend name.
554 * @return Newly allocated checksum text, or NULL on failure.
555 */
556static char *native_sqlite_migration_applied_checksum(const char *db_path,
557 const char *migration_id,
558 const char *backend) {
559 char query[2048];
560 char *escaped_id = NULL;
561 char *escaped_backend = NULL;
562 char *result = NULL;
563 if (db_path == NULL || migration_id == NULL || backend == NULL) {
564 return NULL;
565 }
566 escaped_id = native_sql_literal_escape(migration_id);
567 escaped_backend = native_sql_literal_escape(backend);
568 if (escaped_id == NULL || escaped_backend == NULL) {
569 free(escaped_id);
570 free(escaped_backend);
571 return NULL;
572 }
573 snprintf(query, sizeof(query),
574 "SELECT checksum_sha256 FROM pharos_schema_migration WHERE "
575 "migration_id = '%s' AND backend = '%s' ORDER BY applied_at DESC "
576 "LIMIT 1;",
577 escaped_id, escaped_backend);
578 result = native_sqlite_query_value(db_path, query);
579 free(escaped_id);
580 free(escaped_backend);
581 return result;
582}
583
584/**
585 * @brief Appends a whole file to a growable buffer followed by a newline.
586 * @param buffer Destination buffer.
587 * @param file_path File path to append.
588 * @return 0 on success, otherwise -1.
589 */
590static int native_append_file_to_buffer(Buffer *buffer, const char *file_path) {
591 char *text = read_file_text(file_path);
592 if (text == NULL) {
593 return -1;
594 }
595 if (buffer_append(buffer, text, strlen(text)) != 0 ||
596 buffer_append(buffer, "\n", 1) != 0) {
597 free(text);
598 return -1;
599 }
600 free(text);
601 return 0;
602}
603
604/**
605 * @brief Appends a path marker line to a checksum input buffer.
606 * @param buffer Destination buffer.
607 * @param file_path Source file path.
608 * @return 0 on success, otherwise -1.
609 */
610static int native_append_path_marker(Buffer *buffer, const char *file_path) {
611 char marker[8192];
612 if (buffer == NULL || file_path == NULL) {
613 return -1;
614 }
615 snprintf(marker, sizeof(marker), "\n-- pharos-path: %s\n", file_path);
616 return buffer_append(buffer, marker, strlen(marker));
617}
618
619/**
620 * @brief Computes a sha256 checksum string for arbitrary text.
621 * @param value Input text.
622 * @return Newly allocated checksum hex text, or NULL on failure.
623 */
624static char *native_checksum_string(const char *value) {
625 static const struct {
626 const char *command;
627 const char *arg1;
628 const char *arg2;
629 } checksum_tools[] = {{"sha256sum", NULL, NULL}, {"shasum", "-a", "256"}};
630 char *tmp_path = NULL;
631 char *output = NULL;
632 size_t index;
633 int code = 0;
634 if (native_write_temp_text_file(value, &tmp_path) != 0) {
635 return NULL;
636 }
637 for (index = 0; index < sizeof(checksum_tools) / sizeof(checksum_tools[0]);
638 index++) {
639 if (!native_command_exists(checksum_tools[index].command)) {
640 continue;
641 }
642 output = native_capture_checksum_output(checksum_tools[index].command,
643 checksum_tools[index].arg1,
644 checksum_tools[index].arg2,
645 tmp_path, &code);
646 break;
647 }
648 remove(tmp_path);
649 free(tmp_path);
650 if (code != 0 || output == NULL) {
651 free(output);
652 return NULL;
653 }
654 {
655 char *space = strchr(output, ' ');
656 if (space != NULL) {
657 *space = '\0';
658 }
659 }
660 return output;
661}
662
663/**
664 * @brief Resolves a path string relative to the app root when needed.
665 * @param app_root App root directory.
666 * @param value Raw path value.
667 * @return Newly allocated resolved path, or NULL on failure.
668 */
669static char *native_resolve_app_path(const char *app_root, const char *value) {
670 return resolve_relative_to(app_root, value);
671}
672
673/**
674 * @brief Appends resolved seed source files into a checksum buffer.
675 * @param buffer Destination buffer.
676 * @param app_root App root used for relative seed paths.
677 * @param seed_sources Array of seed source paths.
678 * @return 0 on success, otherwise -1.
679 */
681 const char *app_root,
682 yyjson_val *seed_sources) {
683 yyjson_arr_iter iter;
684 yyjson_val *item = NULL;
685 if (buffer == NULL || app_root == NULL) {
686 return -1;
687 }
688 if (seed_sources == NULL) {
689 return 0;
690 }
691 if (!yyjson_is_arr(seed_sources) || !yyjson_arr_iter_init(seed_sources, &iter)) {
692 return -1;
693 }
694 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
695 char *resolved_path = NULL;
696 const char *raw_path = NULL;
697 if (!yyjson_is_str(item)) {
698 return -1;
699 }
700 raw_path = yyjson_get_str(item);
701 resolved_path = native_resolve_app_path(app_root, raw_path);
702 if (resolved_path == NULL) {
703 return -1;
704 }
705 if (path_exists(resolved_path)) {
706 if (native_append_file_to_buffer(buffer, resolved_path) != 0 ||
707 native_append_path_marker(buffer, resolved_path) != 0) {
708 free(resolved_path);
709 return -1;
710 }
711 }
712 free(resolved_path);
713 }
714 return 0;
715}
716
717/**
718 * @brief Appends a JSON array item into a buffer-backed array literal.
719 * @param buffer Destination buffer.
720 * @param json_item Already serialized JSON item.
721 * @param first Tracks whether the array already has an item.
722 * @return Buffer data on success, or NULL on failure.
723 */
724static char *native_append_json_item(Buffer *buffer, const char *json_item,
725 int *first) {
726 if (buffer == NULL || first == NULL || json_item == NULL) {
727 return NULL;
728 }
729 if (*first) {
730 if (buffer_append(buffer, "[", 1) != 0) {
731 return NULL;
732 }
733 *first = 0;
734 }
735 if (buffer->len > 1 && buffer_append(buffer, ",", 1) != 0) {
736 return NULL;
737 }
738 if (buffer_append(buffer, json_item, strlen(json_item)) != 0) {
739 return NULL;
740 }
741 return buffer->data;
742}
743
744/**
745 * @brief Appends a JSON string item into a buffer-backed array literal.
746 * @param buffer Destination buffer.
747 * @param value Plain string value.
748 * @param first Tracks whether the array already has an item.
749 * @return 0 on success, otherwise -1.
750 */
751static int native_append_json_string_item(Buffer *buffer, const char *value,
752 int *first) {
753 char *json = json_string_dup_native(value != NULL ? value : "");
754 int result = -1;
755 if (json == NULL) {
756 return -1;
757 }
758 result = native_append_json_item(buffer, json, first) != NULL ? 0 : -1;
759 free(json);
760 return result;
761}
762
763/**
764 * @brief Finalizes a buffer-backed JSON array literal.
765 * @param buffer Array buffer.
766 * @param first Whether no items were appended.
767 * @return Newly allocated empty array or buffer-owned array text.
768 */
769static char *native_finish_json_array(Buffer *buffer, int first) {
770 if (buffer == NULL) {
771 return NULL;
772 }
773 if (first) {
774 return native_migrate_verify_strdup("[]");
775 }
776 if (buffer_append(buffer, "]", 1) != 0) {
777 buffer_free(buffer);
778 return NULL;
779 }
780 return buffer->data;
781}
782
783/**
784 * @brief Appends forward SQL file contents for one migration/backend pair.
785 * @param combined Destination checksum input buffer.
786 * @param app_root App root directory.
787 * @param migration_record Migration object.
788 * @param backend Backend name.
789 * @return 0 on success, otherwise -1.
790 */
792 const char *app_root,
793 yyjson_val *migration_record,
794 const char *backend) {
795 yyjson_val *backend_files = NULL;
796 yyjson_val *backend_entry = NULL;
797 yyjson_val *forward_sql_files = NULL;
798 yyjson_arr_iter iter;
799 yyjson_val *item = NULL;
800 if (combined == NULL || app_root == NULL || migration_record == NULL ||
801 backend == NULL) {
802 return -1;
803 }
804 backend_files = native_json_obj_get(migration_record, "backend_files");
805 backend_entry = native_json_obj_get(backend_files, backend);
806 forward_sql_files = native_json_obj_get_array(backend_entry, "forward_sql_files");
807 if (forward_sql_files == NULL) {
808 return 0;
809 }
810 if (!yyjson_arr_iter_init(forward_sql_files, &iter)) {
811 return -1;
812 }
813 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
814 char *resolved_path = NULL;
815 const char *relative_path = NULL;
816 if (!yyjson_is_str(item)) {
817 return -1;
818 }
819 relative_path = yyjson_get_str(item);
820 resolved_path = native_resolve_app_path(app_root, relative_path);
821 if (resolved_path == NULL) {
822 return -1;
823 }
824 if (native_append_file_to_buffer(combined, resolved_path) != 0 ||
825 native_append_path_marker(combined, resolved_path) != 0) {
826 free(resolved_path);
827 return -1;
828 }
829 free(resolved_path);
830 }
831 return 0;
832}
833
834/**
835 * @brief Computes the current expected checksum for a migration.
836 * @param app_root App root directory.
837 * @param migration_record Migration object.
838 * @param entity_fixture Optional entity fixture path.
839 * @param query_fixture Optional query fixture path.
840 * @param backend Backend name.
841 * @return Newly allocated checksum text, or NULL on failure.
842 */
843static char *native_expected_migration_checksum(const char *app_root,
844 yyjson_val *migration_record,
845 const char *entity_fixture,
846 const char *query_fixture,
847 const char *backend) {
848 Buffer combined;
849 char *checksum = NULL;
850 buffer_init(&combined);
851 if (native_append_forward_sql_bundle(&combined, app_root, migration_record,
852 backend) != 0) {
853 buffer_free(&combined);
854 return NULL;
855 }
856 if (entity_fixture != NULL && path_exists(entity_fixture)) {
857 if (native_append_file_to_buffer(&combined, entity_fixture) != 0) {
858 buffer_free(&combined);
859 return NULL;
860 }
861 }
862 if (query_fixture != NULL && path_exists(query_fixture)) {
863 if (native_append_file_to_buffer(&combined, query_fixture) != 0) {
864 buffer_free(&combined);
865 return NULL;
866 }
867 }
868 checksum = native_checksum_string(combined.data != NULL ? combined.data : "");
869 buffer_free(&combined);
870 return checksum;
871}
872
873/**
874 * @brief Computes the legacy expected checksum for a migration.
875 * @param app_root App root directory.
876 * @param migration_record Migration object.
877 * @param entity_fixture Optional entity fixture path.
878 * @param query_fixture Optional query fixture path.
879 * @param backend Backend name.
880 * @param seed_sources Resolved seed source array.
881 * @return Newly allocated checksum text, or NULL on failure.
882 */
884 const char *app_root,
885 yyjson_val *migration_record,
886 const char *entity_fixture,
887 const char *query_fixture,
888 const char *backend,
889 yyjson_val *seed_sources) {
890 char *forward_checksum = NULL;
891 char *entity_checksum = NULL;
892 char *query_checksum = NULL;
893 char *seed_checksum = NULL;
894 char *result = NULL;
895 Buffer payload;
896 yyjson_val *drift_inputs = NULL;
897 int need_entity = 0;
898 int need_query = 0;
899 int need_seed = 0;
900 char *migration_id = NULL;
901
902 if (migration_record == NULL || app_root == NULL || backend == NULL) {
903 return NULL;
904 }
905
906 buffer_init(&payload);
907 drift_inputs = native_json_obj_get_array(migration_record, "drift_inputs");
908 need_entity = native_json_string_array_contains(drift_inputs,
909 "entity_contract_hash");
910 need_query = native_json_string_array_contains(drift_inputs,
911 "query_contract_hash");
912 need_seed = native_json_string_array_contains(drift_inputs,
913 "seed_source_hash");
914
915 forward_checksum = native_expected_migration_checksum(app_root, migration_record,
916 NULL, NULL, backend);
917 if (need_entity && entity_fixture != NULL && path_exists(entity_fixture)) {
918 char *text = read_file_text(entity_fixture);
919 if (text != NULL) {
920 entity_checksum = native_checksum_string(text);
921 free(text);
922 }
923 }
924 if (need_query && query_fixture != NULL && path_exists(query_fixture)) {
925 char *text = read_file_text(query_fixture);
926 if (text != NULL) {
927 query_checksum = native_checksum_string(text);
928 free(text);
929 }
930 }
931 if (need_seed) {
932 Buffer combined;
933 buffer_init(&combined);
934 if (native_append_seed_sources_to_buffer(&combined, app_root, seed_sources) ==
935 0) {
936 seed_checksum = native_checksum_string(combined.data != NULL ? combined.data :
937 "");
938 }
939 buffer_free(&combined);
940 }
941
942 migration_id = native_json_obj_get_string_dup(migration_record, "migration_id");
943 if (buffer_append(&payload, "migration_id=", 13) != 0 ||
944 buffer_append(&payload, migration_id != NULL ? migration_id : "",
945 strlen(migration_id != NULL ? migration_id : "")) != 0 ||
946 buffer_append(&payload, "\nbackend=", 9) != 0 ||
947 buffer_append(&payload, backend, strlen(backend)) != 0 ||
948 buffer_append(&payload, "\nforward_checksum=", 18) != 0 ||
949 buffer_append(&payload,
950 forward_checksum != NULL ? forward_checksum : "",
951 strlen(forward_checksum != NULL ? forward_checksum : "")) != 0 ||
952 buffer_append(&payload, "\nentity_contract_hash=", 22) != 0 ||
953 buffer_append(&payload,
954 entity_checksum != NULL ? entity_checksum : "",
955 strlen(entity_checksum != NULL ? entity_checksum : "")) != 0 ||
956 buffer_append(&payload, "\nquery_contract_hash=", 21) != 0 ||
957 buffer_append(&payload,
958 query_checksum != NULL ? query_checksum : "",
959 strlen(query_checksum != NULL ? query_checksum : "")) != 0 ||
960 buffer_append(&payload, "\nseed_source_hash=", 18) != 0 ||
961 buffer_append(&payload,
962 seed_checksum != NULL ? seed_checksum : "",
963 strlen(seed_checksum != NULL ? seed_checksum : "")) != 0) {
964 buffer_free(&payload);
965 payload.data = NULL;
966 }
967 if (payload.data != NULL) {
968 result = native_checksum_string(payload.data);
969 }
970
971 buffer_free(&payload);
972 free(migration_id);
973 free(forward_checksum);
974 free(entity_checksum);
975 free(query_checksum);
976 free(seed_checksum);
977 return result;
978}
979
980/**
981 * @brief Returns the default sqlite path for an app.
982 * @param app App runtime.
983 * @return Newly allocated sqlite path.
984 */
985static char *native_default_sqlite_path(const AppRuntime *app) {
986 char *db_path =
987 app != NULL ? conf_lookup_value(app->runtime_conf_path, "db_path") : NULL;
988 char buffer[4096];
989 if (db_path != NULL && db_path[0] != '\0') {
990 return db_path;
991 }
992 free(db_path);
993 snprintf(buffer, sizeof(buffer), "/var/lib/pharos/%s.sqlite3",
994 app != NULL && app->app_id != NULL ? app->app_id : "app");
995 return native_migrate_verify_strdup(buffer);
996}
997
999 const AppRuntime *app,
1000 char **status_out,
1001 char **error_out) {
1002 char *manifest_path = NULL;
1003 char *migration_fixture_rel = NULL;
1004 char *entity_fixture_rel = NULL;
1005 char *query_fixture_rel = NULL;
1006 char *migration_fixture = NULL;
1007 char *entity_fixture = NULL;
1008 char *query_fixture = NULL;
1009 char *preferred_backend_json = NULL;
1010 char *development_backend_json = NULL;
1011 char *testing_backend_json = NULL;
1012 char *schema_version_json = NULL;
1013 char *required_tables_json = NULL;
1014 char *required_migration_ids_json = NULL;
1015 char *seed_expectations_json = NULL;
1016 char *verification_contract_json = NULL;
1017 char *sqlite_db_path = NULL;
1018 char *sqlite_db_path_json = NULL;
1019 char *target_id_json = NULL;
1020 char *migration_fixture_json = NULL;
1021 char *entity_fixture_json = NULL;
1022 char *query_fixture_json = NULL;
1023 char *db_config_json = NULL;
1024 char *result = NULL;
1025 char *status_json = NULL;
1026 char *table_checks_json = NULL;
1027 char *seed_checks_json = NULL;
1028 char *migration_checks_json = NULL;
1029 char *missing_tables_json = NULL;
1030 char *missing_migrations_json = NULL;
1031 char *drifted_migrations_json = NULL;
1032 char *failed_seed_expectations_json = NULL;
1033 Buffer table_checks;
1034 Buffer seed_checks;
1035 Buffer migration_checks;
1036 Buffer missing_tables;
1037 Buffer missing_migrations;
1038 Buffer drifted_migrations;
1039 Buffer failed_seed_expectations;
1040 int first_table = 1;
1041 int first_seed = 1;
1042 int first_migration = 1;
1043 int first_missing_table = 1;
1044 int first_missing_migration = 1;
1045 int first_drifted_migration = 1;
1046 int first_failed_seed = 1;
1047 int database_present = 0;
1048 int ledger_table_present = 0;
1049 int verified = 0;
1050 const char *verification_status = "verification_failed";
1051 size_t needed;
1052 yyjson_doc *manifest_doc = NULL;
1053 yyjson_doc *migration_doc = NULL;
1054 yyjson_val *manifest_root = NULL;
1055 yyjson_val *migration_root = NULL;
1056 yyjson_val *database_obj = NULL;
1057 yyjson_val *seed_sources = NULL;
1058 yyjson_val *sqlite_backend = NULL;
1059 yyjson_val *verification_contract = NULL;
1060 yyjson_val *required_tables = NULL;
1061 yyjson_val *required_migration_ids = NULL;
1062 yyjson_val *seed_expectations = NULL;
1063 long schema_version = 0;
1064
1065 if (status_out != NULL) {
1066 *status_out = NULL;
1067 }
1068 if (error_out != NULL) {
1069 *error_out = NULL;
1070 }
1071 if (config == NULL || app == NULL || app->app_root == NULL ||
1072 app->app_id == NULL) {
1073 return NULL;
1074 }
1075 if (!native_command_exists("sqlite3")) {
1076 return NULL;
1077 }
1078
1079 buffer_init(&table_checks);
1080 buffer_init(&seed_checks);
1081 buffer_init(&migration_checks);
1082 buffer_init(&missing_tables);
1083 buffer_init(&missing_migrations);
1084 buffer_init(&drifted_migrations);
1085 buffer_init(&failed_seed_expectations);
1086
1087 manifest_path = path_join(app->app_root, "pharos.app.json");
1088 if (manifest_path == NULL || !path_exists(manifest_path)) {
1089 goto cleanup;
1090 }
1091 manifest_doc = native_json_doc_load_file(manifest_path);
1092 if (manifest_doc == NULL) {
1093 goto cleanup;
1094 }
1095 manifest_root = yyjson_doc_get_root(manifest_doc);
1096 database_obj = native_json_obj_get(manifest_root, "database");
1097 if (database_obj == NULL) {
1098 goto cleanup;
1099 }
1100
1101 migration_fixture_rel =
1102 native_json_obj_get_string_dup(database_obj, "migration_fixture");
1103 entity_fixture_rel = native_json_obj_get_string_dup(database_obj, "entity_fixture");
1104 query_fixture_rel = native_json_obj_get_string_dup(database_obj, "query_fixture");
1105 seed_sources = native_json_obj_get_array(database_obj, "seed_sources");
1106 if (migration_fixture_rel == NULL) {
1107 goto cleanup;
1108 }
1109
1110 migration_fixture = native_resolve_app_path(app->app_root, migration_fixture_rel);
1111 entity_fixture = entity_fixture_rel != NULL
1112 ? native_resolve_app_path(app->app_root, entity_fixture_rel)
1113 : NULL;
1114 query_fixture = query_fixture_rel != NULL
1115 ? native_resolve_app_path(app->app_root, query_fixture_rel)
1116 : NULL;
1117 if (migration_fixture == NULL || !path_exists(migration_fixture)) {
1118 goto cleanup;
1119 }
1120
1121 migration_doc = native_json_doc_load_file(migration_fixture);
1122 if (migration_doc == NULL) {
1123 goto cleanup;
1124 }
1125 migration_root = yyjson_doc_get_root(migration_doc);
1126 sqlite_backend = native_json_find_backend_by_name(migration_root, "sqlite");
1127 verification_contract = native_json_obj_get(sqlite_backend, "verification_contract");
1128 required_tables = native_json_obj_get_array(verification_contract, "required_tables");
1129 required_migration_ids =
1130 native_json_obj_get_array(verification_contract, "required_migration_ids");
1131 seed_expectations =
1132 native_json_obj_get_array(verification_contract, "seed_expectations");
1133
1134 preferred_backend_json =
1135 native_json_object_string_json_dup(migration_root, "preferred_backend",
1136 "sqlite");
1137 development_backend_json =
1138 native_json_object_string_json_dup(migration_root, "development_backend",
1139 "sqlite");
1140 testing_backend_json =
1141 native_json_object_string_json_dup(migration_root, "testing_backend",
1142 "sqlite");
1143 schema_version =
1144 native_json_obj_get_long_default(migration_root, "schema_version", 0, NULL);
1145 schema_version_json = native_long_json_dup(schema_version);
1146 required_tables_json = native_json_value_or_literal_dup(required_tables, "[]");
1147 required_migration_ids_json =
1148 native_json_value_or_literal_dup(required_migration_ids, "[]");
1149 seed_expectations_json =
1150 native_json_value_or_literal_dup(seed_expectations, "[]");
1151 verification_contract_json =
1152 native_json_value_or_literal_dup(verification_contract, "{}");
1153 db_config_json = native_json_literal_dup("{\"backend\":\"sqlite\"}");
1154
1155 sqlite_db_path = native_default_sqlite_path(app);
1156 database_present = sqlite_db_path != NULL && path_exists(sqlite_db_path);
1157 ledger_table_present =
1158 database_present &&
1159 native_sqlite_table_exists(sqlite_db_path, "pharos_schema_migration");
1160
1161 if (required_tables != NULL) {
1162 yyjson_arr_iter iter;
1163 yyjson_val *item = NULL;
1164 if (!yyjson_arr_iter_init(required_tables, &iter)) {
1165 goto cleanup;
1166 }
1167 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1168 const char *table_name = NULL;
1169 int exists = 0;
1170 char *table_json = NULL;
1171 char *item_json = NULL;
1172 size_t item_needed;
1173 if (!yyjson_is_str(item)) {
1174 goto cleanup;
1175 }
1176 table_name = yyjson_get_str(item);
1177 exists = database_present ? native_sqlite_table_exists(sqlite_db_path, table_name)
1178 : 0;
1179 table_json = json_string_dup_native(table_name);
1180 item_needed = (table_json != NULL ? strlen(table_json) : 0) + 96;
1181 item_json = (char *)malloc(item_needed);
1182 if (table_json == NULL || item_json == NULL) {
1183 free(table_json);
1184 free(item_json);
1185 goto cleanup;
1186 }
1187 snprintf(item_json, item_needed,
1188 "{\"table\":%s,\"exists\":%s,\"status\":\"%s\"}",
1189 table_json, exists ? "true" : "false",
1190 exists ? "ok" : "missing_table");
1191 if (native_append_json_item(&table_checks, item_json, &first_table) == NULL) {
1192 free(table_json);
1193 free(item_json);
1194 goto cleanup;
1195 }
1196 if (!exists &&
1197 native_append_json_string_item(&missing_tables, table_name,
1198 &first_missing_table) != 0) {
1199 free(table_json);
1200 free(item_json);
1201 goto cleanup;
1202 }
1203 free(table_json);
1204 free(item_json);
1205 }
1206 }
1207
1208 if (seed_expectations != NULL) {
1209 yyjson_arr_iter iter;
1210 yyjson_val *item = NULL;
1211 if (!yyjson_arr_iter_init(seed_expectations, &iter)) {
1212 goto cleanup;
1213 }
1214 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1215 char *table_name = NULL;
1216 char *table_json = NULL;
1217 char *item_json = NULL;
1218 long minimum_rows = 0;
1219 long exact_rows = 0;
1220 int minimum_present = 0;
1221 int exact_present = 0;
1222 int table_exists = 0;
1223 long row_count = 0;
1224 int satisfied = 0;
1225 const char *reason = "missing_table";
1226 char minimum_buf[64];
1227 char exact_buf[64];
1228 size_t item_needed;
1229 if (!yyjson_is_obj(item)) {
1230 goto cleanup;
1231 }
1232 table_name = native_json_obj_get_string_dup(item, "table");
1233 minimum_rows =
1234 native_json_obj_get_long_default(item, "minimum_rows", 0,
1235 &minimum_present);
1236 exact_rows =
1237 native_json_obj_get_long_default(item, "exact_rows", 0, &exact_present);
1238 if (table_name != NULL && database_present) {
1239 table_exists = native_sqlite_table_exists(sqlite_db_path, table_name);
1240 if (table_exists) {
1241 row_count = native_sqlite_table_row_count(sqlite_db_path, table_name);
1243 row_count, minimum_present, minimum_rows, exact_present, exact_rows,
1244 &reason);
1245 }
1246 }
1247 table_json = json_string_dup_native(table_name != NULL ? table_name : "");
1248 snprintf(minimum_buf, sizeof(minimum_buf), "%ld", minimum_rows);
1249 snprintf(exact_buf, sizeof(exact_buf), "%ld", exact_rows);
1250 item_needed = (table_json != NULL ? strlen(table_json) : 0) +
1251 strlen(reason) + 192;
1252 item_json = (char *)malloc(item_needed);
1253 if (table_json == NULL || item_json == NULL) {
1254 free(table_name);
1255 free(table_json);
1256 free(item_json);
1257 goto cleanup;
1258 }
1259 snprintf(item_json, item_needed,
1260 "{\"table\":%s,\"table_exists\":%s,\"row_count\":%ld,"
1261 "\"minimum_rows\":%s,\"exact_rows\":%s,\"satisfied\":%s,"
1262 "\"reason\":\"%s\"}",
1263 table_json, table_exists ? "true" : "false", row_count,
1264 minimum_present ? minimum_buf : "null",
1265 exact_present ? exact_buf : "null",
1266 satisfied ? "true" : "false", reason);
1267 if (native_append_json_item(&seed_checks, item_json, &first_seed) == NULL) {
1268 free(table_name);
1269 free(table_json);
1270 free(item_json);
1271 goto cleanup;
1272 }
1273 if (!satisfied && native_append_json_item(&failed_seed_expectations, item_json,
1274 &first_failed_seed) == NULL) {
1275 free(table_name);
1276 free(table_json);
1277 free(item_json);
1278 goto cleanup;
1279 }
1280 free(table_name);
1281 free(table_json);
1282 free(item_json);
1283 }
1284 }
1285
1286 if (required_migration_ids != NULL) {
1287 yyjson_arr_iter iter;
1288 yyjson_val *item = NULL;
1289 if (!yyjson_arr_iter_init(required_migration_ids, &iter)) {
1290 goto cleanup;
1291 }
1292 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1293 const char *migration_id = NULL;
1294 yyjson_val *migration_record = NULL;
1295 yyjson_val *accepted_legacy_checksums = NULL;
1296 char *migration_json = NULL;
1297 char *schema_json = NULL;
1298 char *applied_schema_json = NULL;
1299 char *expected_checksum = NULL;
1300 char *legacy_expected_checksum = NULL;
1301 char *applied_checksum = NULL;
1302 char *accepted_legacy_checksums_json = NULL;
1303 char *item_json = NULL;
1304 long applied_count = 0;
1305 int applied = 0;
1306 int checksum_match = 0;
1307 long migration_schema_version = 0;
1308 size_t item_needed;
1309
1310 if (!yyjson_is_str(item)) {
1311 goto cleanup;
1312 }
1313 migration_id = yyjson_get_str(item);
1314 migration_record = native_json_find_migration_by_id(migration_root, migration_id);
1315 accepted_legacy_checksums = native_json_obj_get_array(
1316 native_json_obj_get(migration_record, "accepted_legacy_checksums"),
1317 "sqlite");
1318 migration_schema_version = native_json_obj_get_long_default(
1319 migration_record, "schema_version", 0, NULL);
1320 migration_json = json_string_dup_native(migration_id);
1321 schema_json = native_long_json_dup(migration_schema_version);
1322 accepted_legacy_checksums_json =
1323 native_json_value_or_literal_dup(accepted_legacy_checksums, "[]");
1324 expected_checksum = native_expected_migration_checksum(
1325 app->app_root, migration_record, NULL, NULL, "sqlite");
1326 legacy_expected_checksum = native_legacy_expected_migration_checksum(
1327 app->app_root, migration_record, entity_fixture, query_fixture, "sqlite",
1328 seed_sources);
1329
1330 if (ledger_table_present) {
1331 applied_count = native_sqlite_migration_applied_count(sqlite_db_path,
1332 migration_id,
1333 "sqlite");
1334 applied = applied_count > 0;
1336 sqlite_db_path, migration_id, "sqlite");
1338 sqlite_db_path, migration_id, "sqlite");
1339 checksum_match = native_migration_checksum_matches(
1340 applied, applied_checksum, expected_checksum, legacy_expected_checksum,
1341 accepted_legacy_checksums);
1342 }
1343
1344 item_needed = (migration_json != NULL ? strlen(migration_json) : 0) +
1345 (schema_json != NULL ? strlen(schema_json) : 1) +
1346 (applied_schema_json != NULL ? strlen(applied_schema_json) : 4) +
1347 (expected_checksum != NULL ? strlen(expected_checksum) : 0) +
1348 (legacy_expected_checksum != NULL
1349 ? strlen(legacy_expected_checksum)
1350 : 0) +
1351 (applied_checksum != NULL ? strlen(applied_checksum) : 0) +
1352 (accepted_legacy_checksums_json != NULL
1353 ? strlen(accepted_legacy_checksums_json)
1354 : 2) +
1355 256;
1356 item_json = (char *)malloc(item_needed);
1357 if (migration_json == NULL || schema_json == NULL ||
1358 accepted_legacy_checksums_json == NULL || item_json == NULL) {
1359 free(migration_json);
1360 free(schema_json);
1361 free(applied_schema_json);
1362 free(expected_checksum);
1363 free(legacy_expected_checksum);
1364 free(applied_checksum);
1365 free(accepted_legacy_checksums_json);
1366 free(item_json);
1367 goto cleanup;
1368 }
1369 {
1370 char *expected_json =
1371 json_string_dup_native(expected_checksum != NULL ? expected_checksum : "");
1372 char *legacy_json = json_string_dup_native(
1373 legacy_expected_checksum != NULL ? legacy_expected_checksum : "");
1374 char *applied_checksum_json =
1375 applied_checksum != NULL && applied_checksum[0] != '\0'
1376 ? json_string_dup_native(applied_checksum)
1377 : native_json_literal_dup("null");
1378 if (expected_json == NULL || legacy_json == NULL ||
1379 applied_checksum_json == NULL) {
1380 free(expected_json);
1381 free(legacy_json);
1382 free(applied_checksum_json);
1383 free(migration_json);
1384 free(schema_json);
1385 free(applied_schema_json);
1386 free(expected_checksum);
1387 free(legacy_expected_checksum);
1388 free(applied_checksum);
1389 free(accepted_legacy_checksums_json);
1390 free(item_json);
1391 goto cleanup;
1392 }
1393 snprintf(item_json, item_needed,
1394 "{\"migration_id\":%s,\"backend\":\"sqlite\","
1395 "\"schema_version\":%s,\"applied\":%s,"
1396 "\"applied_count\":%ld,\"applied_schema_version\":%s,"
1397 "\"expected_checksum\":%s,"
1398 "\"legacy_expected_checksum\":%s,"
1399 "\"accepted_legacy_checksums\":%s,"
1400 "\"applied_checksum\":%s,\"checksum_match\":%s}",
1401 migration_json, schema_json, applied ? "true" : "false",
1402 applied_count,
1403 applied_schema_json != NULL && applied_schema_json[0] != '\0'
1404 ? applied_schema_json
1405 : "null",
1406 expected_json, legacy_json, accepted_legacy_checksums_json,
1407 applied_checksum_json, checksum_match ? "true" : "false");
1408 free(expected_json);
1409 free(legacy_json);
1410 free(applied_checksum_json);
1411 }
1412 if (native_append_json_item(&migration_checks, item_json, &first_migration) ==
1413 NULL) {
1414 free(migration_json);
1415 free(schema_json);
1416 free(applied_schema_json);
1417 free(expected_checksum);
1418 free(legacy_expected_checksum);
1419 free(applied_checksum);
1420 free(accepted_legacy_checksums_json);
1421 free(item_json);
1422 goto cleanup;
1423 }
1424 if (!applied &&
1425 native_append_json_string_item(&missing_migrations, migration_id,
1426 &first_missing_migration) != 0) {
1427 free(migration_json);
1428 free(schema_json);
1429 free(applied_schema_json);
1430 free(expected_checksum);
1431 free(legacy_expected_checksum);
1432 free(applied_checksum);
1433 free(accepted_legacy_checksums_json);
1434 free(item_json);
1435 goto cleanup;
1436 }
1437 if (applied && !checksum_match &&
1438 native_append_json_string_item(&drifted_migrations, migration_id,
1439 &first_drifted_migration) != 0) {
1440 free(migration_json);
1441 free(schema_json);
1442 free(applied_schema_json);
1443 free(expected_checksum);
1444 free(legacy_expected_checksum);
1445 free(applied_checksum);
1446 free(accepted_legacy_checksums_json);
1447 free(item_json);
1448 goto cleanup;
1449 }
1450 free(migration_json);
1451 free(schema_json);
1452 free(applied_schema_json);
1453 free(expected_checksum);
1454 free(legacy_expected_checksum);
1455 free(applied_checksum);
1456 free(accepted_legacy_checksums_json);
1457 free(item_json);
1458 }
1459 }
1460
1461 table_checks_json = native_finish_json_array(&table_checks, first_table);
1462 seed_checks_json = native_finish_json_array(&seed_checks, first_seed);
1463 migration_checks_json = native_finish_json_array(&migration_checks, first_migration);
1464 missing_tables_json = native_finish_json_array(&missing_tables, first_missing_table);
1465 missing_migrations_json =
1466 native_finish_json_array(&missing_migrations, first_missing_migration);
1467 drifted_migrations_json =
1468 native_finish_json_array(&drifted_migrations, first_drifted_migration);
1469 failed_seed_expectations_json =
1470 native_finish_json_array(&failed_seed_expectations, first_failed_seed);
1471 if (table_checks_json == NULL || seed_checks_json == NULL ||
1472 migration_checks_json == NULL || missing_tables_json == NULL ||
1473 missing_migrations_json == NULL || drifted_migrations_json == NULL ||
1474 failed_seed_expectations_json == NULL) {
1475 goto cleanup;
1476 }
1477
1478 verified = database_present && ledger_table_present &&
1479 streq(missing_tables_json, "[]") &&
1480 streq(missing_migrations_json, "[]") &&
1481 streq(drifted_migrations_json, "[]") &&
1482 streq(failed_seed_expectations_json, "[]");
1483 verification_status = verified ? "verified" : "verification_failed";
1484 if (status_out != NULL) {
1485 *status_out = native_migrate_verify_strdup(verification_status);
1486 }
1487
1488 target_id_json = json_string_dup_native(config->target_id != NULL
1489 ? config->target_id
1490 : app->app_id);
1491 sqlite_db_path_json = json_string_dup_native(sqlite_db_path != NULL ? sqlite_db_path : "");
1492 migration_fixture_json = json_string_dup_native(migration_fixture != NULL ? migration_fixture : "");
1493 entity_fixture_json = native_json_nullable_string_dup(entity_fixture);
1494 query_fixture_json = native_json_nullable_string_dup(query_fixture);
1495 status_json = json_string_dup_native(verification_status);
1496
1497 needed = (target_id_json != NULL ? strlen(target_id_json) : 0) +
1498 (status_json != NULL ? strlen(status_json) : 0) +
1499 (preferred_backend_json != NULL ? strlen(preferred_backend_json) : 8) +
1500 (development_backend_json != NULL ? strlen(development_backend_json) : 8) +
1501 (testing_backend_json != NULL ? strlen(testing_backend_json) : 8) +
1502 (schema_version_json != NULL ? strlen(schema_version_json) : 1) +
1503 (required_tables_json != NULL ? strlen(required_tables_json) : 2) +
1504 (required_migration_ids_json != NULL
1505 ? strlen(required_migration_ids_json)
1506 : 2) +
1507 (seed_expectations_json != NULL ? strlen(seed_expectations_json) : 2) +
1508 (verification_contract_json != NULL
1509 ? strlen(verification_contract_json)
1510 : 2) +
1511 (sqlite_db_path_json != NULL ? strlen(sqlite_db_path_json) : 0) +
1512 (migration_fixture_json != NULL ? strlen(migration_fixture_json) : 0) +
1513 (entity_fixture_json != NULL ? strlen(entity_fixture_json) : 4) +
1514 (query_fixture_json != NULL ? strlen(query_fixture_json) : 4) +
1515 (db_config_json != NULL ? strlen(db_config_json) : 20) +
1516 (table_checks_json != NULL ? strlen(table_checks_json) : 2) +
1517 (missing_tables_json != NULL ? strlen(missing_tables_json) : 2) +
1518 (seed_checks_json != NULL ? strlen(seed_checks_json) : 2) +
1519 (failed_seed_expectations_json != NULL
1520 ? strlen(failed_seed_expectations_json)
1521 : 2) +
1522 (migration_checks_json != NULL ? strlen(migration_checks_json) : 2) +
1523 (missing_migrations_json != NULL ? strlen(missing_migrations_json) : 2) +
1524 (drifted_migrations_json != NULL ? strlen(drifted_migrations_json) : 2) +
1525 1024;
1526 result = (char *)malloc(needed);
1527 if (result != NULL) {
1528 snprintf(result, needed,
1529 "{\"report_id\":\"pharos-migrate-verify-v1\","
1530 "\"target_kind\":\"app\",\"target_id\":%s,"
1531 "\"backend\":\"sqlite\",\"status\":%s,"
1532 "\"live_database_verified\":%s,"
1533 "\"database_present\":%s,\"ledger_table_present\":%s,"
1534 "\"db_path\":%s,\"schema_version\":%s,"
1535 "\"preferred_backend\":%s,\"development_backend\":%s,"
1536 "\"testing_backend\":%s,\"migration_fixture\":%s,"
1537 "\"entity_fixture\":%s,\"query_fixture\":%s,"
1538 "\"db_config\":%s,\"verification_contract\":%s,"
1539 "\"required_tables\":%s,\"required_migration_ids\":%s,"
1540 "\"seed_expectations\":%s,\"table_checks\":%s,"
1541 "\"missing_tables\":%s,\"seed_checks\":%s,"
1542 "\"failed_seed_expectations\":%s,\"migration_checks\":%s,"
1543 "\"missing_migrations\":%s,\"drifted_migrations\":%s}",
1544 target_id_json,
1545 status_json != NULL ? status_json : "\"verification_failed\"",
1546 verified ? "true" : "false",
1547 database_present ? "true" : "false",
1548 ledger_table_present ? "true" : "false",
1549 sqlite_db_path_json != NULL ? sqlite_db_path_json : "\"\"",
1550 schema_version_json != NULL ? schema_version_json : "0",
1551 preferred_backend_json != NULL ? preferred_backend_json : "\"sqlite\"",
1552 development_backend_json != NULL ? development_backend_json : "\"sqlite\"",
1553 testing_backend_json != NULL ? testing_backend_json : "\"sqlite\"",
1554 migration_fixture_json != NULL ? migration_fixture_json : "\"\"",
1555 entity_fixture_json != NULL ? entity_fixture_json : "null",
1556 query_fixture_json != NULL ? query_fixture_json : "null",
1557 db_config_json != NULL ? db_config_json : "{\"backend\":\"sqlite\"}",
1558 verification_contract_json != NULL ? verification_contract_json : "{}",
1559 required_tables_json != NULL ? required_tables_json : "[]",
1560 required_migration_ids_json != NULL ? required_migration_ids_json : "[]",
1561 seed_expectations_json != NULL ? seed_expectations_json : "[]",
1562 table_checks_json != NULL ? table_checks_json : "[]",
1563 missing_tables_json != NULL ? missing_tables_json : "[]",
1564 seed_checks_json != NULL ? seed_checks_json : "[]",
1565 failed_seed_expectations_json != NULL ? failed_seed_expectations_json : "[]",
1566 migration_checks_json != NULL ? migration_checks_json : "[]",
1567 missing_migrations_json != NULL ? missing_migrations_json : "[]",
1568 drifted_migrations_json != NULL ? drifted_migrations_json : "[]");
1569 }
1570
1571cleanup:
1572 if (result == NULL) {
1573 char *failure_target_id_json = target_id_json != NULL
1574 ? NULL
1575 : json_string_dup_native(config->target_id != NULL
1576 ? config->target_id
1577 : app->app_id);
1578 char *failure_status_json =
1579 status_json != NULL ? NULL : json_string_dup_native("verification_failed");
1580 char *failure_db_path_json = sqlite_db_path_json != NULL
1581 ? NULL
1582 : json_string_dup_native(sqlite_db_path != NULL
1583 ? sqlite_db_path
1584 : "");
1585 char *failure_migration_fixture_json =
1586 migration_fixture_json != NULL
1587 ? NULL
1588 : native_json_nullable_string_dup(migration_fixture);
1589 char *failure_entity_fixture_json =
1590 entity_fixture_json != NULL ? NULL : native_json_nullable_string_dup(entity_fixture);
1591 char *failure_query_fixture_json =
1592 query_fixture_json != NULL ? NULL : native_json_nullable_string_dup(query_fixture);
1593 char *effective_target_id_json =
1594 target_id_json != NULL ? target_id_json : failure_target_id_json;
1595 char *effective_status_json =
1596 status_json != NULL ? status_json : failure_status_json;
1597 char *effective_db_path_json =
1598 sqlite_db_path_json != NULL ? sqlite_db_path_json : failure_db_path_json;
1599 char *effective_migration_fixture_json =
1600 migration_fixture_json != NULL ? migration_fixture_json : failure_migration_fixture_json;
1601 char *effective_entity_fixture_json =
1602 entity_fixture_json != NULL ? entity_fixture_json : failure_entity_fixture_json;
1603 char *effective_query_fixture_json =
1604 query_fixture_json != NULL ? query_fixture_json : failure_query_fixture_json;
1605 if (status_out != NULL && *status_out == NULL) {
1606 *status_out = native_migrate_verify_strdup("verification_failed");
1607 }
1608 if (error_out != NULL && *error_out == NULL) {
1609 *error_out = native_migrate_verify_strdup("native sqlite migrate verify unavailable");
1610 }
1611 if (effective_target_id_json != NULL && effective_status_json != NULL &&
1612 effective_db_path_json != NULL && effective_migration_fixture_json != NULL &&
1613 effective_entity_fixture_json != NULL && effective_query_fixture_json != NULL) {
1614 size_t failure_needed = strlen(effective_target_id_json) +
1615 strlen(effective_status_json) +
1616 strlen(effective_db_path_json) +
1617 strlen(effective_migration_fixture_json) +
1618 strlen(effective_entity_fixture_json) +
1619 strlen(effective_query_fixture_json) + 2048;
1620 result = (char *)malloc(failure_needed);
1621 if (result != NULL) {
1622 snprintf(result, failure_needed,
1623 "{\"report_id\":\"pharos-migrate-verify-v1\","
1624 "\"target_kind\":\"app\",\"target_id\":%s,"
1625 "\"backend\":\"sqlite\",\"status\":%s,"
1626 "\"live_database_verified\":false,"
1627 "\"database_present\":%s,\"ledger_table_present\":%s,"
1628 "\"db_path\":%s,\"schema_version\":0,"
1629 "\"preferred_backend\":\"sqlite\","
1630 "\"development_backend\":\"sqlite\","
1631 "\"testing_backend\":\"sqlite\","
1632 "\"migration_fixture\":%s,\"entity_fixture\":%s,"
1633 "\"query_fixture\":%s,\"db_config\":{\"backend\":\"sqlite\"},"
1634 "\"verification_contract\":{},\"required_tables\":[],"
1635 "\"required_migration_ids\":[],\"seed_expectations\":[],"
1636 "\"table_checks\":[],\"missing_tables\":[],"
1637 "\"seed_checks\":[],\"failed_seed_expectations\":[],"
1638 "\"migration_checks\":[],\"missing_migrations\":[],"
1639 "\"drifted_migrations\":[]}",
1640 effective_target_id_json, effective_status_json,
1641 database_present ? "true" : "false",
1642 ledger_table_present ? "true" : "false",
1643 effective_db_path_json, effective_migration_fixture_json,
1644 effective_entity_fixture_json, effective_query_fixture_json);
1645 }
1646 }
1647 return result;
1648 }
1649
1650 native_json_doc_free(manifest_doc);
1651 native_json_doc_free(migration_doc);
1652
1653 free(manifest_path);
1654 free(migration_fixture_rel);
1655 free(entity_fixture_rel);
1656 free(query_fixture_rel);
1657 free(migration_fixture);
1658 free(entity_fixture);
1659 free(query_fixture);
1660 free(preferred_backend_json);
1661 free(development_backend_json);
1662 free(testing_backend_json);
1663 free(schema_version_json);
1664 free(required_tables_json);
1665 free(required_migration_ids_json);
1666 free(seed_expectations_json);
1667 free(verification_contract_json);
1668 free(sqlite_db_path);
1669 free(sqlite_db_path_json);
1670 free(target_id_json);
1671 free(migration_fixture_json);
1672 free(entity_fixture_json);
1673 free(query_fixture_json);
1674 free(db_config_json);
1675 free(status_json);
1676
1677 if (table_checks_json != NULL && table_checks_json != table_checks.data) {
1678 free(table_checks_json);
1679 }
1680 if (seed_checks_json != NULL && seed_checks_json != seed_checks.data) {
1681 free(seed_checks_json);
1682 }
1683 if (migration_checks_json != NULL &&
1684 migration_checks_json != migration_checks.data) {
1685 free(migration_checks_json);
1686 }
1687 if (missing_tables_json != NULL && missing_tables_json != missing_tables.data) {
1688 free(missing_tables_json);
1689 }
1690 if (missing_migrations_json != NULL &&
1691 missing_migrations_json != missing_migrations.data) {
1692 free(missing_migrations_json);
1693 }
1694 if (drifted_migrations_json != NULL &&
1695 drifted_migrations_json != drifted_migrations.data) {
1696 free(drifted_migrations_json);
1697 }
1698 if (failed_seed_expectations_json != NULL &&
1699 failed_seed_expectations_json != failed_seed_expectations.data) {
1700 free(failed_seed_expectations_json);
1701 }
1702
1703 buffer_free(&table_checks);
1704 buffer_free(&seed_checks);
1705 buffer_free(&migration_checks);
1706 buffer_free(&missing_tables);
1707 buffer_free(&missing_migrations);
1708 buffer_free(&drifted_migrations);
1709 buffer_free(&failed_seed_expectations);
1710
1711 return result;
1712}
1713
1714#else
1715
1717 const AppRuntime *app,
1718 char **status_out,
1719 char **error_out) {
1720 (void)config;
1721 (void)app;
1722 if (status_out != NULL) {
1723 *status_out = NULL;
1724 }
1725 if (error_out != NULL) {
1726 *error_out = NULL;
1727 }
1728 return NULL;
1729}
1730
1731#endif
char * json_string_dup_native(const char *value)
Duplicates a string while escaping it for JSON string output.
Declares helpers that build native artifact and report payload strings.
char * conf_lookup_value(const char *path, const char *key)
Reads a Pharos conf file and looks up one key.
Declares lightweight JSON and conf value lookup helpers.
static int native_command_exists(const char *command)
Tests whether a required native tool is available.
static char * native_resolve_command_path(const char *command)
Resolves an executable from PATH.
static char * native_default_sqlite_path(const AppRuntime *app)
Returns the default sqlite path for an app.
static int native_seed_expectation_satisfied(long row_count, int minimum_present, long minimum_rows, int exact_present, long exact_rows, const char **reason_out)
Evaluates whether a seed expectation passes.
static int native_json_string_array_contains(yyjson_val *array_value, const char *expected_value)
Tests whether a JSON string array contains one exact string.
static int native_append_seed_sources_to_buffer(Buffer *buffer, const char *app_root, yyjson_val *seed_sources)
Appends resolved seed source files into a checksum buffer.
static char * native_capture_trimmed(char *const argv[], int *code_out)
Runs a subprocess and trims surrounding whitespace from stdout.
static char * native_append_json_item(Buffer *buffer, const char *json_item, int *first)
Appends a JSON array item into a buffer-backed array literal.
static char * native_capture_checksum_output(const char *command, const char *arg1, const char *arg2, const char *file_path, int *code_out)
Captures checksum tool output for a temporary file.
static char * native_resolve_app_path(const char *app_root, const char *value)
Resolves a path string relative to the app root when needed.
static int native_write_temp_text_file(const char *text, char **path_out)
Writes temporary text content to a file.
static char * native_json_value_or_literal_dup(yyjson_val *value, const char *fallback)
Duplicates a borrowed JSON value as compact JSON, or a fallback literal.
static char * native_json_object_string_json_dup(yyjson_val *obj, const char *key, const char *fallback)
Duplicates an object string field as JSON string text with a default.
static char * native_sqlite_query_value(const char *db_path, const char *sql)
Executes a sqlite query and captures one trimmed stdout value.
static char * native_json_nullable_string_dup(const char *value)
Duplicates a string value as JSON string text, or null when absent.
static char * native_legacy_expected_migration_checksum(const char *app_root, yyjson_val *migration_record, const char *entity_fixture, const char *query_fixture, const char *backend, yyjson_val *seed_sources)
Computes the legacy expected checksum for a migration.
static int native_append_json_string_item(Buffer *buffer, const char *value, int *first)
Appends a JSON string item into a buffer-backed array literal.
static char * native_sqlite_migration_applied_schema_version(const char *db_path, const char *migration_id, const char *backend)
Reads the applied schema version for a migration.
static char * native_finish_json_array(Buffer *buffer, int first)
Finalizes a buffer-backed JSON array literal.
char * native_capture_migrate_verify_sqlite_report_json(const ServeConfig *config, const AppRuntime *app, char **status_out, char **error_out)
Captures a sqlite migration verification report for one app.
static char * native_sql_literal_escape(const char *value)
Escapes a string for safe inclusion in a sqlite string literal.
static long native_sqlite_migration_applied_count(const char *db_path, const char *migration_id, const char *backend)
Counts applied migration ledger rows.
static char * native_checksum_string(const char *value)
Computes a sha256 checksum string for arbitrary text.
static int native_identifier_safe(const char *value)
Tests whether a string is a safe sqlite identifier.
static char * native_long_json_dup(long value)
Converts a long value to owned JSON number text.
static char * native_json_literal_dup(const char *value)
Duplicates a static JSON literal string.
static int native_migration_checksum_matches(int applied, const char *applied_checksum, const char *expected_checksum, const char *legacy_expected_checksum, yyjson_val *accepted_legacy_checksums)
Tests whether an applied checksum satisfies expected checksum contracts.
static char * native_sqlite_migration_applied_checksum(const char *db_path, const char *migration_id, const char *backend)
Reads the applied checksum for a migration.
#define native_migrate_verify_strdup
static int native_append_path_marker(Buffer *buffer, const char *file_path)
Appends a path marker line to a checksum input buffer.
static int native_append_file_to_buffer(Buffer *buffer, const char *file_path)
Appends a whole file to a growable buffer followed by a newline.
static int native_sqlite_table_exists(const char *db_path, const char *table_name)
Tests whether a sqlite table exists.
static int native_append_escaped_sql_char(Buffer *buffer, char value)
Appends one SQL character with sqlite literal escaping.
static char * native_expected_migration_checksum(const char *app_root, yyjson_val *migration_record, const char *entity_fixture, const char *query_fixture, const char *backend)
Computes the current expected checksum for a migration.
static long native_sqlite_table_row_count(const char *db_path, const char *table_name)
Counts rows in a sqlite table.
static int native_append_forward_sql_bundle(Buffer *combined, const char *app_root, yyjson_val *migration_record, const char *backend)
Appends forward SQL file contents for one migration/backend pair.
Declares native migration verification entry points.
int run_process_capture_native(char *const argv[], char **output)
Executes a process and captures its combined output text.
Declares child-process capture helpers.
void buffer_init(Buffer *buffer)
Initializes a growable buffer to an empty state.
char * path_join(const char *left, const char *right)
Joins two path segments using the native path separator.
void trim_in_place(char *text)
Trims leading and trailing ASCII whitespace from a string in place.
void buffer_free(Buffer *buffer)
Releases memory owned by a growable buffer.
int streq(const char *a, const char *b)
Tests whether two strings are exactly equal.
char * resolve_relative_to(const char *base_dir, const char *value)
Resolves a path value relative to a base directory when needed.
int buffer_append(Buffer *buffer, const void *data, size_t len)
Appends bytes to a growable buffer and maintains a trailing NUL byte.
char * read_file_text(const char *path)
Reads an entire file into a newly allocated text buffer.
int path_exists(const char *path)
Tests whether a filesystem path currently exists.
Declares shared low-level support helpers used across the Pharos native runtime.
yyjson_doc * native_json_doc_load_file(const char *path)
Parses a JSON file into an owned yyjson document.
yyjson_val * native_json_find_migration_by_id(yyjson_val *root, const char *migration_id)
Finds a migration record by migration_id under a fixture root.
long native_json_obj_get_long_default(yyjson_val *obj, const char *key, long default_value, int *found_out)
Reads a numeric object member or returns a fallback value.
void native_json_doc_free(yyjson_doc *doc)
Releases a document returned by the native yyjson helpers.
yyjson_val * native_json_find_backend_by_name(yyjson_val *root, const char *backend_name)
Finds a backend record by backend name under a fixture root.
char * native_json_obj_get_string_dup(yyjson_val *obj, const char *key)
Duplicates a string-valued key from a JSON object.
yyjson_val * native_json_obj_get_array(yyjson_val *obj, const char *key)
Looks up an array-valued key from a JSON object value.
yyjson_val * native_json_obj_get(yyjson_val *obj, const char *key)
Looks up a key from a JSON object value.
char * native_json_serialize_compact_dup(yyjson_val *value)
Serializes a JSON value to compact JSON text.
Declares shared helper wrappers around vendored yyjson parsing and serialization APIs.
Loaded runtime metadata for one Pharos app artifact.
Growable byte buffer used by parsing and string assembly helpers.
size_t len
char * data
Parsed native serve configuration used to bootstrap the runtime listener.
yyjson_api_inline bool yyjson_is_str(const yyjson_val *val)
Definition yyjson.h:5390
yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val)
Definition yyjson.h:5398
yyjson_api_inline yyjson_val * yyjson_doc_get_root(const yyjson_doc *doc)
Definition yyjson.h:5323
yyjson_api_inline bool yyjson_is_arr(const yyjson_val *val)
Definition yyjson.h:5394
yyjson_api_inline bool yyjson_equals_str(const yyjson_val *val, const char *str)
Definition yyjson.h:5477
yyjson_api_inline yyjson_val * yyjson_arr_iter_next(yyjson_arr_iter *iter)
Definition yyjson.h:5672
yyjson_api_inline const char * yyjson_get_str(const yyjson_val *val)
Definition yyjson.h:5469
yyjson_api_inline bool yyjson_arr_iter_init(const yyjson_val *arr, yyjson_arr_iter *iter)
Definition yyjson.h:5650