| 731 | } |
| 732 | |
| 733 | cbm_store_t *cbm_store_open_path_query(const char *db_path) { |
| 734 | if (!db_path) { |
| 735 | return NULL; |
| 736 | } |
| 737 | |
| 738 | cbm_store_t *s = calloc(CBM_ALLOC_ONE, sizeof(cbm_store_t)); |
| 739 | if (!s) { |
| 740 | return NULL; |
| 741 | } |
| 742 | |
| 743 | /* Query tools open the project DB READ-ONLY: a read query must never |
| 744 | * mutate the DB (the previous READWRITE open + WAL write-pragmas did), |
| 745 | * and must work on a read-only DB file / filesystem. |
| 746 | * |
| 747 | * Try a plain READONLY open first — on a normal writable filesystem this |
| 748 | * reads WAL frames correctly via the -shm wal-index. SQLite opens lazily, |
| 749 | * so a read-only-filesystem failure (cannot create -shm for a WAL-mode |
| 750 | * DB) surfaces on first access, not at open time; we probe with a trivial |
| 751 | * read to force it. If the probe fails, retry once with an immutable URI |
| 752 | * that bypasses WAL and reads the main DB file directly. |
| 753 | * |
| 754 | * No SQLITE_OPEN_CREATE on either path — a missing DB must return NULL |
| 755 | * (no ghost .db for unknown/unindexed projects). */ |
| 756 | int rc = sqlite3_open_v2(db_path, &s->db, SQLITE_OPEN_READONLY, NULL); |
| 757 | if (rc == SQLITE_OK) { |
| 758 | /* Force first DB access so a read-only-FS WAL failure surfaces now. */ |
| 759 | if (sqlite3_exec(s->db, "SELECT 1 FROM sqlite_master LIMIT 1;", NULL, NULL, NULL) != |
| 760 | SQLITE_OK) { |
| 761 | sqlite3_close(s->db); |
| 762 | s->db = NULL; |
| 763 | rc = SQLITE_CANTOPEN; /* trigger immutable fallback */ |
| 764 | } |
| 765 | } |
| 766 | if (rc != SQLITE_OK) { |
| 767 | sqlite3_close(s->db); /* no-op if already NULL */ |
| 768 | s->db = NULL; |
| 769 | /* A genuinely missing DB must return NULL without creating anything — |
| 770 | * only retry with the immutable URI when the file exists but could not |
| 771 | * be opened (the read-only-filesystem case). This also keeps the |
| 772 | * common "project not found" path to a single open attempt. */ |
| 773 | if (!cbm_file_exists(db_path)) { |
| 774 | free(s); |
| 775 | return NULL; |
| 776 | } |
| 777 | char uri[ST_QUERY_URI_MAX]; |
| 778 | if (!build_immutable_uri(db_path, uri, sizeof(uri))) { |
| 779 | free(s); |
| 780 | return NULL; |
| 781 | } |
| 782 | rc = sqlite3_open_v2(uri, &s->db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL); |
| 783 | if (rc != SQLITE_OK) { |
| 784 | /* sqlite3_open_v2 allocates a handle even on failure — must close it. */ |
| 785 | sqlite3_close(s->db); |
| 786 | free(s); |
| 787 | return NULL; |
| 788 | } |
| 789 | } |
| 790 | |