| 834 | } |
| 835 | |
| 836 | cbm_store_t *cbm_store_open_path_query(const char *db_path) { |
| 837 | if (!db_path) { |
| 838 | return NULL; |
| 839 | } |
| 840 | |
| 841 | cbm_store_t *s = calloc(CBM_ALLOC_ONE, sizeof(cbm_store_t)); |
| 842 | if (!s) { |
| 843 | return NULL; |
| 844 | } |
| 845 | |
| 846 | /* Query tools open the project DB READ-ONLY: a read query must never |
| 847 | * mutate the DB (the previous READWRITE open + WAL write-pragmas did), |
| 848 | * and must work on a read-only DB file / filesystem. |
| 849 | * |
| 850 | * Try a plain READONLY open first — on a normal writable filesystem this |
| 851 | * reads WAL frames correctly via the -shm wal-index. SQLite opens lazily, |
| 852 | * so a read-only-filesystem failure (cannot create -shm for a WAL-mode |
| 853 | * DB) surfaces on first access, not at open time; we probe with a trivial |
| 854 | * read to force it. If the probe fails, retry once with an immutable URI |
| 855 | * that bypasses WAL and reads the main DB file directly. |
| 856 | * |
| 857 | * No SQLITE_OPEN_CREATE on either path — a missing DB must return NULL |
| 858 | * (no ghost .db for unknown/unindexed projects). */ |
| 859 | char open_path[4096]; |
| 860 | if (!cbm_path_for_file_api(db_path, open_path, sizeof(open_path))) { |
| 861 | free(s); |
| 862 | return NULL; |
| 863 | } |
| 864 | int rc = sqlite3_open_v2(open_path, &s->db, SQLITE_OPEN_READONLY, NULL); |
| 865 | if (rc == SQLITE_OK) { |
| 866 | /* Force first DB access so a read-only-FS WAL failure surfaces now. */ |
| 867 | if (sqlite3_exec(s->db, "SELECT 1 FROM sqlite_master LIMIT 1;", NULL, NULL, NULL) != |
| 868 | SQLITE_OK) { |
| 869 | sqlite3_close(s->db); |
| 870 | s->db = NULL; |
| 871 | rc = SQLITE_CANTOPEN; /* trigger immutable fallback */ |
| 872 | } |
| 873 | } |
| 874 | if (rc != SQLITE_OK) { |
| 875 | sqlite3_close(s->db); /* no-op if already NULL */ |
| 876 | s->db = NULL; |
| 877 | /* A genuinely missing DB must return NULL without creating anything — |
| 878 | * only retry with the immutable URI when the file exists but could not |
| 879 | * be opened (the read-only-filesystem case). This also keeps the |
| 880 | * common "project not found" path to a single open attempt. */ |
| 881 | if (!cbm_file_exists(db_path)) { |
| 882 | free(s); |
| 883 | return NULL; |
| 884 | } |
| 885 | char uri[ST_QUERY_URI_MAX]; |
| 886 | if (!build_immutable_uri(db_path, uri, sizeof(uri))) { |
| 887 | free(s); |
| 888 | return NULL; |
| 889 | } |
| 890 | rc = sqlite3_open_v2(uri, &s->db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL); |
| 891 | if (rc != SQLITE_OK) { |
| 892 | /* sqlite3_open_v2 allocates a handle even on failure — must close it. */ |
| 893 | sqlite3_close(s->db); |