@chipmobilesdk/rn-local-db 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -102
- package/README.md +728 -728
- package/package.json +59 -62
- package/skills/sdk-rn-local-db/SKILL.md +0 -1
- package/src/adapter/budgets.ts +67 -67
- package/src/adapter/collection.ts +303 -298
- package/src/adapter/deletion.ts +252 -252
- package/src/adapter/index.ts +64 -68
- package/src/adapter/lifecycle.ts +517 -520
- package/src/adapter/transaction.ts +71 -71
- package/src/artifacts/availability.ts +67 -67
- package/src/artifacts/cascade.ts +114 -114
- package/src/artifacts/paths.ts +29 -29
- package/src/artifacts/reconcile.ts +63 -63
- package/src/artifacts/store.ts +334 -345
- package/src/contract/adapter.ts +76 -78
- package/src/contract/attachment.ts +75 -78
- package/src/contract/backup.ts +28 -28
- package/src/contract/config.ts +54 -54
- package/src/contract/encryption.ts +34 -34
- package/src/contract/errors.ts +76 -76
- package/src/contract/migration.ts +42 -42
- package/src/contract/query.ts +81 -81
- package/src/contract/record.ts +46 -51
- package/src/contract/schema.ts +36 -36
- package/src/contract/scope.ts +36 -36
- package/src/contract/transaction.ts +52 -54
- package/src/crypto/chunked-cipher.ts +153 -150
- package/src/crypto/derive.ts +64 -64
- package/src/crypto/key-provider.ts +78 -78
- package/src/crypto/provider.ts +81 -81
- package/src/diagnostics/dev-log.ts +112 -108
- package/src/diagnostics/errors.ts +62 -66
- package/src/diagnostics/faults.ts +58 -64
- package/src/diagnostics/map-native.ts +108 -106
- package/src/diagnostics/redact.ts +137 -141
- package/src/index.ts +80 -110
- package/src/migration/executor.ts +132 -136
- package/src/migration/ledger.ts +95 -97
- package/src/migration/validate-chain.ts +65 -65
- package/src/platform/backup-android-verify.ts +88 -88
- package/src/platform/backup-ios.ts +48 -48
- package/src/platform/bytes.ts +144 -144
- package/src/platform/fs.ts +89 -89
- package/src/platform/location.ts +135 -135
- package/src/query/budgets.ts +54 -54
- package/src/query/compare.ts +91 -91
- package/src/query/cursor.ts +100 -100
- package/src/query/filter.ts +82 -82
- package/src/query/paginate.ts +51 -51
- package/src/query/sort.ts +55 -56
- package/src/query/validate.ts +171 -171
- package/src/schema/declare.ts +42 -45
- package/src/schema/validate-config.ts +158 -158
- package/src/scope/hash.ts +131 -134
- package/src/scope/normalize.ts +83 -90
- package/src/scope/validate.ts +83 -83
- package/src/sqlite/adapter.ts +26 -26
- package/src/sqlite/batch.ts +242 -242
- package/src/sqlite/catalog.ts +105 -108
- package/src/sqlite/connection.ts +227 -227
- package/src/sqlite/index-coverage.ts +54 -56
- package/src/sqlite/native.ts +83 -83
- package/src/sqlite/row-codec.ts +217 -221
- package/src/sqlite/sql-builder.ts +244 -244
- package/src/sqlite/transaction-queue.ts +87 -87
- package/src/sqlite/transactions.ts +33 -33
- package/src/sqlite/values.ts +24 -24
- package/src/testing/conformance/attachments.ts +138 -136
- package/src/testing/conformance/encryption.ts +150 -150
- package/src/testing/conformance/index.ts +161 -161
- package/src/testing/conformance/lifecycle.ts +99 -99
- package/src/testing/conformance/migrations.ts +177 -177
- package/src/testing/conformance/queries.ts +204 -167
- package/src/testing/conformance/records.ts +169 -169
- package/src/testing/conformance/scopes.ts +86 -88
- package/src/testing/conformance/transactions.ts +149 -149
- package/src/testing/faults.ts +21 -21
- package/src/testing/index.ts +17 -21
- package/src/testing/memory-adapter/collection.ts +214 -219
- package/src/testing/memory-adapter/index.ts +510 -492
- package/src/testing/memory-adapter/migrations.ts +117 -119
- package/src/testing/memory-adapter/reset.ts +50 -50
- package/src/testing/memory-adapter/store.ts +128 -134
|
@@ -1,56 +1,54 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* "Is this query covered by an index?", answered by the engine rather than by
|
|
3
|
-
* inspection of the declarations (FR-037).
|
|
4
|
-
*
|
|
5
|
-
* Reading the schema would answer what the developer intended; `EXPLAIN QUERY
|
|
6
|
-
* PLAN` answers what SQLite will actually do, and the two diverge exactly
|
|
7
|
-
* where it matters — a filter on an indexed field that the planner declines
|
|
8
|
-
* because of a type mismatch or a leading wildcard still reports as "indexed"
|
|
9
|
-
* if you only read declarations.
|
|
10
|
-
*
|
|
11
|
-
* Exposed as a development-mode diagnostic. It runs an extra statement, so an
|
|
12
|
-
* app calling it on every list would pay for it.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { IndexCoverageReport, Query, SortKey } from '../contract/query';
|
|
16
|
-
import type { CollectionSchema } from '../contract/schema';
|
|
17
|
-
import { collectionTable } from '../schema/declare';
|
|
18
|
-
import type { Executor } from './connection';
|
|
19
|
-
import { buildFilter, buildOrderBy } from './sql-builder';
|
|
20
|
-
import { declaredIndexName } from './row-codec';
|
|
21
|
-
|
|
22
|
-
const SCAN = /\bSCAN\b/i;
|
|
23
|
-
const USING_INDEX = /USING\s+(COVERING\s+)?INDEX\s+"?([A-Za-z0-9_]+)"?/i;
|
|
24
|
-
|
|
25
|
-
export async function explainQuery(
|
|
26
|
-
executor: Executor,
|
|
27
|
-
schema: CollectionSchema,
|
|
28
|
-
query: Query | undefined,
|
|
29
|
-
sort: SortKey[],
|
|
30
|
-
): Promise<IndexCoverageReport> {
|
|
31
|
-
const filter = buildFilter(query?.filter);
|
|
32
|
-
const table = collectionTable(schema.name);
|
|
33
|
-
const sql = `EXPLAIN QUERY PLAN SELECT id FROM "${table}" WHERE ${filter.sql} ORDER BY ${buildOrderBy(
|
|
34
|
-
sort,
|
|
35
|
-
)}`;
|
|
36
|
-
|
|
37
|
-
const result = await executor.execute(sql, filter.params);
|
|
38
|
-
const detail = result.rows
|
|
39
|
-
.map(row => String(row.detail ?? row.DETAIL ?? ''))
|
|
40
|
-
.filter(Boolean)
|
|
41
|
-
.join('; ');
|
|
42
|
-
|
|
43
|
-
const match = USING_INDEX.exec(detail);
|
|
44
|
-
return {
|
|
45
|
-
collection: schema.name,
|
|
46
|
-
usesIndex: match !== null,
|
|
47
|
-
// Reported under the name the app declared, not the collection-scoped
|
|
48
|
-
// name it has in the file.
|
|
49
|
-
indexName: match?.[2] === undefined ? undefined : declaredIndexName(schema, match[2]),
|
|
50
|
-
detail:
|
|
51
|
-
detail ||
|
|
52
|
-
(SCAN.test(detail)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* "Is this query covered by an index?", answered by the engine rather than by
|
|
3
|
+
* inspection of the declarations (FR-037).
|
|
4
|
+
*
|
|
5
|
+
* Reading the schema would answer what the developer intended; `EXPLAIN QUERY
|
|
6
|
+
* PLAN` answers what SQLite will actually do, and the two diverge exactly
|
|
7
|
+
* where it matters — a filter on an indexed field that the planner declines
|
|
8
|
+
* because of a type mismatch or a leading wildcard still reports as "indexed"
|
|
9
|
+
* if you only read declarations.
|
|
10
|
+
*
|
|
11
|
+
* Exposed as a development-mode diagnostic. It runs an extra statement, so an
|
|
12
|
+
* app calling it on every list would pay for it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { IndexCoverageReport, Query, SortKey } from '../contract/query';
|
|
16
|
+
import type { CollectionSchema } from '../contract/schema';
|
|
17
|
+
import { collectionTable } from '../schema/declare';
|
|
18
|
+
import type { Executor } from './connection';
|
|
19
|
+
import { buildFilter, buildOrderBy } from './sql-builder';
|
|
20
|
+
import { declaredIndexName } from './row-codec';
|
|
21
|
+
|
|
22
|
+
const SCAN = /\bSCAN\b/i;
|
|
23
|
+
const USING_INDEX = /USING\s+(COVERING\s+)?INDEX\s+"?([A-Za-z0-9_]+)"?/i;
|
|
24
|
+
|
|
25
|
+
export async function explainQuery(
|
|
26
|
+
executor: Executor,
|
|
27
|
+
schema: CollectionSchema,
|
|
28
|
+
query: Query | undefined,
|
|
29
|
+
sort: SortKey[],
|
|
30
|
+
): Promise<IndexCoverageReport> {
|
|
31
|
+
const filter = buildFilter(query?.filter);
|
|
32
|
+
const table = collectionTable(schema.name);
|
|
33
|
+
const sql = `EXPLAIN QUERY PLAN SELECT id FROM "${table}" WHERE ${filter.sql} ORDER BY ${buildOrderBy(
|
|
34
|
+
sort,
|
|
35
|
+
)}`;
|
|
36
|
+
|
|
37
|
+
const result = await executor.execute(sql, filter.params);
|
|
38
|
+
const detail = result.rows
|
|
39
|
+
.map(row => String(row.detail ?? row.DETAIL ?? ''))
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('; ');
|
|
42
|
+
|
|
43
|
+
const match = USING_INDEX.exec(detail);
|
|
44
|
+
return {
|
|
45
|
+
collection: schema.name,
|
|
46
|
+
usesIndex: match !== null,
|
|
47
|
+
// Reported under the name the app declared, not the collection-scoped
|
|
48
|
+
// name it has in the file.
|
|
49
|
+
indexName: match?.[2] === undefined ? undefined : declaredIndexName(schema, match[2]),
|
|
50
|
+
detail:
|
|
51
|
+
detail ||
|
|
52
|
+
(SCAN.test(detail) ? 'full table scan' : 'the engine returned no plan for this query'),
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/sqlite/native.ts
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The storage engine peer, behind one lazy accessor.
|
|
3
|
-
*
|
|
4
|
-
* Every native call the package makes to SQLite passes through this type.
|
|
5
|
-
* Note what is missing: `executeSync`. op-sqlite ships it, and using it would
|
|
6
|
-
* put a database read on the interface thread — the one thing PERF-001's
|
|
7
|
-
* 16-millisecond rule cannot survive (research.md R9). A surface that cannot
|
|
8
|
-
* express the synchronous call is a stronger guarantee than a review comment.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { DiagnosticCodes } from '../contract/errors';
|
|
12
|
-
import { raise } from '../diagnostics/errors';
|
|
13
|
-
|
|
14
|
-
export type SqlValue = string | number | boolean | null | undefined;
|
|
15
|
-
|
|
16
|
-
export type ExecuteResult = {
|
|
17
|
-
rows: Record<string, unknown>[];
|
|
18
|
-
rowsAffected: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type NativeTransaction = {
|
|
22
|
-
execute(sql: string, params?: SqlValue[]): Promise<ExecuteResult>;
|
|
23
|
-
commit(): Promise<void>;
|
|
24
|
-
rollback(): Promise<void>;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type NativeDatabase = {
|
|
28
|
-
execute(sql: string, params?: SqlValue[]): Promise<ExecuteResult>;
|
|
29
|
-
executeBatch(commands: [string, SqlValue[]?][]): Promise<ExecuteResult>;
|
|
30
|
-
transaction<T>(fn: (tx: NativeTransaction) => Promise<T>): Promise<T>;
|
|
31
|
-
close(): Promise<void>;
|
|
32
|
-
delete(): Promise<void>;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type OpenOptions = {
|
|
36
|
-
name: string;
|
|
37
|
-
location?: string;
|
|
38
|
-
/** SQLCipher only. Never logged, never persisted, never returned (FR-098). */
|
|
39
|
-
encryptionKey?: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type Engine = {
|
|
43
|
-
open(options: OpenOptions): NativeDatabase;
|
|
44
|
-
isSQLCipher?: () => boolean;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
let override: Engine | null = null;
|
|
48
|
-
let cached: Engine | null = null;
|
|
49
|
-
|
|
50
|
-
/** Swap the engine. Tests only. */
|
|
51
|
-
export function setEngine(next: Engine | null): void {
|
|
52
|
-
override = next;
|
|
53
|
-
cached = null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function engine(): Engine {
|
|
57
|
-
if (override) {
|
|
58
|
-
return override;
|
|
59
|
-
}
|
|
60
|
-
if (!cached) {
|
|
61
|
-
try {
|
|
62
|
-
cached = require('@op-engineering/op-sqlite') as Engine;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
raise(DiagnosticCodes.OPEN_FAILED, {
|
|
65
|
-
message: 'storage engine peer is not installed',
|
|
66
|
-
cause: error,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return cached!;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Whether SQLCipher is compiled into this build.
|
|
75
|
-
*
|
|
76
|
-
* A build flag in the consuming app, not a separate peer (research.md R1a).
|
|
77
|
-
* An app that enables encryption without it must fail loudly rather than
|
|
78
|
-
* write plaintext to a file it believes is encrypted.
|
|
79
|
-
*/
|
|
80
|
-
export function isEncryptionAvailable(): boolean {
|
|
81
|
-
const current = engine();
|
|
82
|
-
return typeof current.isSQLCipher === 'function' ? current.isSQLCipher() === true : false;
|
|
83
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The storage engine peer, behind one lazy accessor.
|
|
3
|
+
*
|
|
4
|
+
* Every native call the package makes to SQLite passes through this type.
|
|
5
|
+
* Note what is missing: `executeSync`. op-sqlite ships it, and using it would
|
|
6
|
+
* put a database read on the interface thread — the one thing PERF-001's
|
|
7
|
+
* 16-millisecond rule cannot survive (research.md R9). A surface that cannot
|
|
8
|
+
* express the synchronous call is a stronger guarantee than a review comment.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DiagnosticCodes } from '../contract/errors';
|
|
12
|
+
import { raise } from '../diagnostics/errors';
|
|
13
|
+
|
|
14
|
+
export type SqlValue = string | number | boolean | null | undefined;
|
|
15
|
+
|
|
16
|
+
export type ExecuteResult = {
|
|
17
|
+
rows: Record<string, unknown>[];
|
|
18
|
+
rowsAffected: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type NativeTransaction = {
|
|
22
|
+
execute(sql: string, params?: SqlValue[]): Promise<ExecuteResult>;
|
|
23
|
+
commit(): Promise<void>;
|
|
24
|
+
rollback(): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type NativeDatabase = {
|
|
28
|
+
execute(sql: string, params?: SqlValue[]): Promise<ExecuteResult>;
|
|
29
|
+
executeBatch(commands: [string, SqlValue[]?][]): Promise<ExecuteResult>;
|
|
30
|
+
transaction<T>(fn: (tx: NativeTransaction) => Promise<T>): Promise<T>;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
delete(): Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type OpenOptions = {
|
|
36
|
+
name: string;
|
|
37
|
+
location?: string;
|
|
38
|
+
/** SQLCipher only. Never logged, never persisted, never returned (FR-098). */
|
|
39
|
+
encryptionKey?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type Engine = {
|
|
43
|
+
open(options: OpenOptions): NativeDatabase;
|
|
44
|
+
isSQLCipher?: () => boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let override: Engine | null = null;
|
|
48
|
+
let cached: Engine | null = null;
|
|
49
|
+
|
|
50
|
+
/** Swap the engine. Tests only. */
|
|
51
|
+
export function setEngine(next: Engine | null): void {
|
|
52
|
+
override = next;
|
|
53
|
+
cached = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function engine(): Engine {
|
|
57
|
+
if (override) {
|
|
58
|
+
return override;
|
|
59
|
+
}
|
|
60
|
+
if (!cached) {
|
|
61
|
+
try {
|
|
62
|
+
cached = require('@op-engineering/op-sqlite') as Engine;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
raise(DiagnosticCodes.OPEN_FAILED, {
|
|
65
|
+
message: 'storage engine peer is not installed',
|
|
66
|
+
cause: error,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return cached!;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether SQLCipher is compiled into this build.
|
|
75
|
+
*
|
|
76
|
+
* A build flag in the consuming app, not a separate peer (research.md R1a).
|
|
77
|
+
* An app that enables encryption without it must fail loudly rather than
|
|
78
|
+
* write plaintext to a file it believes is encrypted.
|
|
79
|
+
*/
|
|
80
|
+
export function isEncryptionAvailable(): boolean {
|
|
81
|
+
const current = engine();
|
|
82
|
+
return typeof current.isSQLCipher === 'function' ? current.isSQLCipher() === true : false;
|
|
83
|
+
}
|