Create internal db debugger

This commit is contained in:
Jamie Kyle 2025-08-20 13:00:14 -07:00 committed by GitHub
commit ae7c2c09a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 130 additions and 3 deletions

View file

@ -1462,3 +1462,9 @@ $secondary-text-color: light-dark(
max-width: 360px; max-width: 360px;
} }
} }
.Preferences__ReadonlySqlPlayground__Textarea {
&__input {
font-family: variables.$monospace;
}
}

View file

@ -37,6 +37,7 @@ export type PropsType = {
onFocus?: () => unknown; onFocus?: () => unknown;
onEnter?: () => unknown; onEnter?: () => unknown;
placeholder: string; placeholder: string;
readOnly?: boolean;
value?: string; value?: string;
whenToShowRemainingCount?: number; whenToShowRemainingCount?: number;
whenToWarnRemainingCount?: number; whenToWarnRemainingCount?: number;
@ -84,6 +85,7 @@ export const Input = forwardRef<
onFocus, onFocus,
onEnter, onEnter,
placeholder, placeholder,
readOnly,
value = '', value = '',
whenToShowRemainingCount = Infinity, whenToShowRemainingCount = Infinity,
whenToWarnRemainingCount = Infinity, whenToWarnRemainingCount = Infinity,
@ -226,6 +228,7 @@ export const Input = forwardRef<
onKeyDown: handleKeyDown, onKeyDown: handleKeyDown,
onPaste: handlePaste, onPaste: handlePaste,
placeholder, placeholder,
readOnly,
ref: refMerger<HTMLInputElement | HTMLTextAreaElement | null>( ref: refMerger<HTMLInputElement | HTMLTextAreaElement | null>(
ref, ref,
innerRef innerRef

View file

@ -497,6 +497,9 @@ export default {
action('generateDonationReceiptBlob')(); action('generateDonationReceiptBlob')();
return new Blob(); return new Blob();
}, },
__dangerouslyRunAbitraryReadOnlySqlQuery: async () => {
return Promise.resolve([]);
},
} satisfies PropsType, } satisfies PropsType,
} satisfies Meta<PropsType>; } satisfies Meta<PropsType>;

View file

@ -14,6 +14,7 @@ import { isNumber, noop, partition } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import * as LocaleMatcher from '@formatjs/intl-localematcher'; import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MutableRefObject, ReactNode } from 'react'; import type { MutableRefObject, ReactNode } from 'react';
import type { RowType } from '@signalapp/sqlcipher';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { ChatColorPicker } from './ChatColorPicker'; import { ChatColorPicker } from './ChatColorPicker';
import { Checkbox } from './Checkbox'; import { Checkbox } from './Checkbox';
@ -312,6 +313,9 @@ type PropsFunctionType = {
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>; onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>; onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>; onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
__dangerouslyRunAbitraryReadOnlySqlQuery: (
readonlySqlQuery: string
) => Promise<ReadonlyArray<RowType<object>>>;
// Localization // Localization
i18n: LocalizerType; i18n: LocalizerType;
@ -511,6 +515,7 @@ export function Preferences({
internalAddDonationReceipt, internalAddDonationReceipt,
saveAttachmentToDisk, saveAttachmentToDisk,
generateDonationReceiptBlob, generateDonationReceiptBlob,
__dangerouslyRunAbitraryReadOnlySqlQuery,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const storiesId = useId(); const storiesId = useId();
const themeSelectId = useId(); const themeSelectId = useId();
@ -2198,6 +2203,9 @@ export function Preferences({
internalAddDonationReceipt={internalAddDonationReceipt} internalAddDonationReceipt={internalAddDonationReceipt}
saveAttachmentToDisk={saveAttachmentToDisk} saveAttachmentToDisk={saveAttachmentToDisk}
generateDonationReceiptBlob={generateDonationReceiptBlob} generateDonationReceiptBlob={generateDonationReceiptBlob}
__dangerouslyRunAbitraryReadOnlySqlQuery={
__dangerouslyRunAbitraryReadOnlySqlQuery
}
/> />
} }
contentsRef={settingsPaneRef} contentsRef={settingsPaneRef}

View file

@ -1,10 +1,11 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback, useRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { RowType } from '@signalapp/sqlcipher';
import type { LocalizerType } from '../types/I18N'; import type { LocalizerType } from '../types/I18N';
import { toLogFormat } from '../types/errors'; import { toLogFormat } from '../types/errors';
import { formatFileSize } from '../util/formatFileSize'; import { formatFileSize } from '../util/formatFileSize';
@ -19,6 +20,7 @@ import type { DonationReceipt } from '../types/Donations';
import { createLogger } from '../logging/log'; import { createLogger } from '../logging/log';
import { isStagingServer } from '../util/isStagingServer'; import { isStagingServer } from '../util/isStagingServer';
import { getHumanDonationAmount } from '../util/currency'; import { getHumanDonationAmount } from '../util/currency';
import { AutoSizeTextArea } from './AutoSizeTextArea';
const log = createLogger('PreferencesInternal'); const log = createLogger('PreferencesInternal');
@ -32,6 +34,7 @@ export function PreferencesInternal({
internalAddDonationReceipt, internalAddDonationReceipt,
saveAttachmentToDisk, saveAttachmentToDisk,
generateDonationReceiptBlob, generateDonationReceiptBlob,
__dangerouslyRunAbitraryReadOnlySqlQuery,
}: { }: {
i18n: LocalizerType; i18n: LocalizerType;
exportLocalBackup: () => Promise<BackupValidationResultType>; exportLocalBackup: () => Promise<BackupValidationResultType>;
@ -51,6 +54,9 @@ export function PreferencesInternal({
receipt: DonationReceipt, receipt: DonationReceipt,
i18n: LocalizerType i18n: LocalizerType
) => Promise<Blob>; ) => Promise<Blob>;
__dangerouslyRunAbitraryReadOnlySqlQuery: (
readonlySqlQuery: string
) => Promise<ReadonlyArray<RowType<object>>>;
}): JSX.Element { }): JSX.Element {
const [isExportPending, setIsExportPending] = useState(false); const [isExportPending, setIsExportPending] = useState(false);
const [exportResult, setExportResult] = useState< const [exportResult, setExportResult] = useState<
@ -68,6 +74,11 @@ export function PreferencesInternal({
BackupValidationResultType | undefined BackupValidationResultType | undefined
>(); >();
const [readOnlySqlInput, setReadOnlySqlInput] = useState('');
const [readOnlySqlResults, setReadOnlySqlResults] = useState<ReadonlyArray<
RowType<object>
> | null>(null);
const validateBackup = useCallback(async () => { const validateBackup = useCallback(async () => {
setIsValidationPending(true); setIsValidationPending(true);
setValidationResult(undefined); setValidationResult(undefined);
@ -184,6 +195,34 @@ export function PreferencesInternal({
[i18n, saveAttachmentToDisk, generateDonationReceiptBlob] [i18n, saveAttachmentToDisk, generateDonationReceiptBlob]
); );
const handleReadonlySqlInputChange = useCallback(
(newReadonlySqlInput: string) => {
setReadOnlySqlInput(newReadonlySqlInput);
},
[]
);
const prevAbortControlerRef = useRef<AbortController | null>(null);
const handleReadOnlySqlInputSubmit = useCallback(async () => {
const controller = new AbortController();
const { signal } = controller;
prevAbortControlerRef.current?.abort();
prevAbortControlerRef.current = controller;
setReadOnlySqlResults(null);
const result =
await __dangerouslyRunAbitraryReadOnlySqlQuery(readOnlySqlInput);
if (signal.aborted) {
return;
}
setReadOnlySqlResults(result);
}, [readOnlySqlInput, __dangerouslyRunAbitraryReadOnlySqlQuery]);
return ( return (
<div className="Preferences--internal"> <div className="Preferences--internal">
<SettingsRow <SettingsRow
@ -437,6 +476,34 @@ export function PreferencesInternal({
)} )}
</SettingsRow> </SettingsRow>
)} )}
<SettingsRow title="Readonly SQL Playground">
<FlowingSettingsControl>
<AutoSizeTextArea
i18n={i18n}
value={readOnlySqlInput}
onChange={handleReadonlySqlInputChange}
placeholder="SELECT * FROM items"
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
/>
<Button
variant={ButtonVariant.Destructive}
onClick={handleReadOnlySqlInputSubmit}
>
Run Query
</Button>
{readOnlySqlResults != null && (
<AutoSizeTextArea
i18n={i18n}
value={JSON.stringify(readOnlySqlResults, null, 2)}
onChange={() => null}
readOnly
placeholder=""
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
/>
)}
</FlowingSettingsControl>
</SettingsRow>
</div> </div>
); );
} }

View file

@ -1,7 +1,7 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/sqlcipher'; import type { Database, RowType } from '@signalapp/sqlcipher';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
@ -894,6 +894,10 @@ type ReadableInterface = {
getMessageSampleForSchemaVersion: ( getMessageSampleForSchemaVersion: (
version: number version: number
) => Array<MessageAttributesType>; ) => Array<MessageAttributesType>;
__dangerouslyRunAbitraryReadOnlySqlQuery: (
readOnlySqlQuery: string
) => ReadonlyArray<RowType<object>>;
}; };
type WritableInterface = { type WritableInterface = {

View file

@ -4,6 +4,7 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
// TODO(indutny): format queries // TODO(indutny): format queries
import type { RowType } from '@signalapp/sqlcipher';
import SQL, { setLogger as setSqliteLogger } from '@signalapp/sqlcipher'; import SQL, { setLogger as setSqliteLogger } from '@signalapp/sqlcipher';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { mkdirSync, rmSync } from 'node:fs'; import { mkdirSync, rmSync } from 'node:fs';
@ -477,6 +478,8 @@ export const DataReader: ServerReadableInterface = {
finishPageMessages, finishPageMessages,
getKnownDownloads, getKnownDownloads,
getKnownConversationAttachments, getKnownConversationAttachments,
__dangerouslyRunAbitraryReadOnlySqlQuery,
}; };
export const DataWriter: ServerWritableInterface = { export const DataWriter: ServerWritableInterface = {
@ -8717,3 +8720,17 @@ function ensureMessageInsertTriggersAreEnabled(db: WritableDB): void {
} }
})(); })();
} }
function __dangerouslyRunAbitraryReadOnlySqlQuery(
db: ReadableDB,
readOnlySqlQuery: string
): ReadonlyArray<RowType<object>> {
let results: ReadonlyArray<RowType<object>>;
try {
db.pragma('query_only = on');
results = db.prepare(readOnlySqlQuery).all();
} finally {
db.pragma('query_only = off');
}
return results;
}

View file

@ -1,7 +1,7 @@
// Copyright 2024 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { StrictMode, useEffect } from 'react'; import React, { StrictMode, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { AudioDevice } from '@signalapp/ringrtc'; import type { AudioDevice } from '@signalapp/ringrtc';
@ -686,6 +686,15 @@ export function SmartPreferences(): JSX.Element | null {
} }
); );
const __dangerouslyRunAbitraryReadOnlySqlQuery = useCallback(
(readOnlySqlQuery: string) => {
return DataReader.__dangerouslyRunAbitraryReadOnlySqlQuery(
readOnlySqlQuery
);
},
[]
);
if (currentLocation.tab !== NavTab.Settings) { if (currentLocation.tab !== NavTab.Settings) {
return null; return null;
} }
@ -883,6 +892,9 @@ export function SmartPreferences(): JSX.Element | null {
internalAddDonationReceipt={internalAddDonationReceipt} internalAddDonationReceipt={internalAddDonationReceipt}
saveAttachmentToDisk={window.Signal.Migrations.saveAttachmentToDisk} saveAttachmentToDisk={window.Signal.Migrations.saveAttachmentToDisk}
generateDonationReceiptBlob={generateDonationReceiptBlob} generateDonationReceiptBlob={generateDonationReceiptBlob}
__dangerouslyRunAbitraryReadOnlySqlQuery={
__dangerouslyRunAbitraryReadOnlySqlQuery
}
/> />
</StrictMode> </StrictMode>
); );

View file

@ -1501,6 +1501,13 @@
"updated": "2025-05-30T22:48:14.420Z", "updated": "2025-05-30T22:48:14.420Z",
"reasonDetail": "For focusing the settings backup key viewer textarea" "reasonDetail": "For focusing the settings backup key viewer textarea"
}, },
{
"rule": "React-useRef",
"path": "ts/components/PreferencesInternal.tsx",
"line": " const prevAbortControlerRef = useRef<AbortController | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-08-20T18:18:34.081Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/ProfileEditor.tsx", "path": "ts/components/ProfileEditor.tsx",