@chipmobilesdk/rn-local-db 0.1.2 → 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 -52
- package/README.md +663 -661
- 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 -238
- package/src/adapter/index.ts +64 -68
- package/src/adapter/lifecycle.ts +517 -444
- package/src/adapter/transaction.ts +71 -65
- package/src/artifacts/availability.ts +67 -69
- package/src/artifacts/cascade.ts +114 -78
- package/src/artifacts/paths.ts +29 -29
- package/src/artifacts/reconcile.ts +63 -63
- package/src/artifacts/store.ts +334 -323
- package/src/contract/adapter.ts +76 -78
- package/src/contract/attachment.ts +75 -72
- 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 -40
- package/src/contract/query.ts +81 -81
- package/src/contract/record.ts +46 -42
- package/src/contract/schema.ts +36 -36
- package/src/contract/scope.ts +36 -36
- package/src/contract/transaction.ts +52 -46
- package/src/crypto/chunked-cipher.ts +153 -144
- package/src/crypto/derive.ts +64 -63
- 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 -0
- package/src/platform/fs.ts +89 -89
- package/src/platform/location.ts +135 -135
- package/src/query/budgets.ts +54 -57
- package/src/query/compare.ts +91 -91
- package/src/query/cursor.ts +100 -99
- 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 -232
- package/src/sqlite/catalog.ts +105 -108
- package/src/sqlite/connection.ts +227 -205
- package/src/sqlite/index-coverage.ts +54 -53
- package/src/sqlite/native.ts +83 -83
- package/src/sqlite/row-codec.ts +217 -201
- package/src/sqlite/sql-builder.ts +244 -244
- package/src/sqlite/transaction-queue.ts +87 -0
- 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 -388
- 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
package/src/sqlite/row-codec.ts
CHANGED
|
@@ -1,201 +1,217 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Table shape, and the payload-as-truth rule.
|
|
3
|
-
*
|
|
4
|
-
* Every read reconstructs the record from `payload` alone. The projected
|
|
5
|
-
* `f_*` columns are written on every write and consulted only by `WHERE` and
|
|
6
|
-
* `ORDER BY` — never by a read path (research.md R6).
|
|
7
|
-
*
|
|
8
|
-
* That single rule is what makes two requirements structural rather than
|
|
9
|
-
* carefully maintained. FR-018's byte fidelity holds because nothing else is
|
|
10
|
-
* consulted to rebuild a record, and FR-019's explicit-null versus
|
|
11
|
-
* absent-field distinction holds because a NULL projected column cannot be
|
|
12
|
-
* ambiguous if no read ever looks at it.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { StorableRecordData, StoredRecord } from '../contract/record';
|
|
16
|
-
import type { CollectionSchema, FieldDeclaration } from '../contract/schema';
|
|
17
|
-
import { collectionTable, hasSoftDelete, hasTimestamps, projectedColumn } from '../schema/declare';
|
|
18
|
-
import type { SqlValue } from './native';
|
|
19
|
-
import { num, text } from './values';
|
|
20
|
-
|
|
21
|
-
export const RESERVED_COLUMNS = ['id', 'payload', 'created_at', 'updated_at', 'deleted_at'];
|
|
22
|
-
|
|
23
|
-
/** SQLite affinity for a declared field type. */
|
|
24
|
-
function affinityOf(field: FieldDeclaration): string {
|
|
25
|
-
switch (field.type) {
|
|
26
|
-
case 'number':
|
|
27
|
-
case 'timestamp':
|
|
28
|
-
return 'REAL';
|
|
29
|
-
case 'boolean':
|
|
30
|
-
return 'INTEGER';
|
|
31
|
-
case 'string':
|
|
32
|
-
default:
|
|
33
|
-
return 'TEXT';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function createTableSql(schema: CollectionSchema): string {
|
|
38
|
-
const columns = [
|
|
39
|
-
'id TEXT PRIMARY KEY',
|
|
40
|
-
'payload TEXT NOT NULL',
|
|
41
|
-
'created_at INTEGER',
|
|
42
|
-
'updated_at INTEGER',
|
|
43
|
-
'deleted_at INTEGER',
|
|
44
|
-
...(schema.fields ?? []).map(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Table shape, and the payload-as-truth rule.
|
|
3
|
+
*
|
|
4
|
+
* Every read reconstructs the record from `payload` alone. The projected
|
|
5
|
+
* `f_*` columns are written on every write and consulted only by `WHERE` and
|
|
6
|
+
* `ORDER BY` — never by a read path (research.md R6).
|
|
7
|
+
*
|
|
8
|
+
* That single rule is what makes two requirements structural rather than
|
|
9
|
+
* carefully maintained. FR-018's byte fidelity holds because nothing else is
|
|
10
|
+
* consulted to rebuild a record, and FR-019's explicit-null versus
|
|
11
|
+
* absent-field distinction holds because a NULL projected column cannot be
|
|
12
|
+
* ambiguous if no read ever looks at it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { StorableRecordData, StoredRecord } from '../contract/record';
|
|
16
|
+
import type { CollectionSchema, FieldDeclaration } from '../contract/schema';
|
|
17
|
+
import { collectionTable, hasSoftDelete, hasTimestamps, projectedColumn } from '../schema/declare';
|
|
18
|
+
import type { SqlValue } from './native';
|
|
19
|
+
import { num, text } from './values';
|
|
20
|
+
|
|
21
|
+
export const RESERVED_COLUMNS = ['id', 'payload', 'created_at', 'updated_at', 'deleted_at'];
|
|
22
|
+
|
|
23
|
+
/** SQLite affinity for a declared field type. */
|
|
24
|
+
function affinityOf(field: FieldDeclaration): string {
|
|
25
|
+
switch (field.type) {
|
|
26
|
+
case 'number':
|
|
27
|
+
case 'timestamp':
|
|
28
|
+
return 'REAL';
|
|
29
|
+
case 'boolean':
|
|
30
|
+
return 'INTEGER';
|
|
31
|
+
case 'string':
|
|
32
|
+
default:
|
|
33
|
+
return 'TEXT';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createTableSql(schema: CollectionSchema): string {
|
|
38
|
+
const columns = [
|
|
39
|
+
'id TEXT PRIMARY KEY',
|
|
40
|
+
'payload TEXT NOT NULL',
|
|
41
|
+
'created_at INTEGER',
|
|
42
|
+
'updated_at INTEGER',
|
|
43
|
+
'deleted_at INTEGER',
|
|
44
|
+
...(schema.fields ?? []).map(field => `"${projectedColumn(field.name)}" ${affinityOf(field)}`),
|
|
45
|
+
];
|
|
46
|
+
return `CREATE TABLE IF NOT EXISTS "${collectionTable(schema.name)}" (${columns.join(', ')})`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function addColumnSql(schema: CollectionSchema, field: FieldDeclaration): string {
|
|
50
|
+
return `ALTER TABLE "${collectionTable(schema.name)}" ADD COLUMN "${projectedColumn(
|
|
51
|
+
field.name,
|
|
52
|
+
)}" ${affinityOf(field)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The name an index has in the database file.
|
|
57
|
+
*
|
|
58
|
+
* Index names are database-wide in SQLite, but declared per collection. Until
|
|
59
|
+
* 0.2.0 the declared name was used as is, so a second collection declaring an
|
|
60
|
+
* index of the same name hit `IF NOT EXISTS` and silently got no index — a
|
|
61
|
+
* UNIQUE constraint included. The double underscore keeps a declared name from
|
|
62
|
+
* colliding with the package's own `<table>_live` index.
|
|
63
|
+
*/
|
|
64
|
+
export function physicalIndexName(schema: CollectionSchema, declaredName: string): string {
|
|
65
|
+
return `${collectionTable(schema.name)}__${declaredName}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The declared name back from a physical one, for reporting to the app. */
|
|
69
|
+
export function declaredIndexName(schema: CollectionSchema, physicalName: string): string {
|
|
70
|
+
const prefix = `${collectionTable(schema.name)}__`;
|
|
71
|
+
return physicalName.startsWith(prefix) ? physicalName.slice(prefix.length) : physicalName;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createIndexSql(schema: CollectionSchema): string[] {
|
|
75
|
+
const table = collectionTable(schema.name);
|
|
76
|
+
const statements = (schema.indexes ?? []).map(index => {
|
|
77
|
+
const columns = index.fields.map(field => `"${projectedColumn(field)}"`).join(', ');
|
|
78
|
+
return `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${physicalIndexName(
|
|
79
|
+
schema,
|
|
80
|
+
index.name,
|
|
81
|
+
)}" ON "${table}" (${columns})`;
|
|
82
|
+
});
|
|
83
|
+
if (hasSoftDelete(schema)) {
|
|
84
|
+
// Every default query carries `deleted_at IS NULL`, so without this the
|
|
85
|
+
// exclusion turns the cheapest possible filter into a full scan.
|
|
86
|
+
statements.push(`CREATE INDEX IF NOT EXISTS "${table}_live" ON "${table}" (deleted_at)`);
|
|
87
|
+
}
|
|
88
|
+
return statements;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Project a declared field for the WHERE and ORDER BY columns.
|
|
93
|
+
*
|
|
94
|
+
* Anything that is not a comparable primitive projects to NULL. A nested
|
|
95
|
+
* object has no defensible ordering, and inventing one would make a filter
|
|
96
|
+
* silently match records a consumer never meant to select.
|
|
97
|
+
*/
|
|
98
|
+
export function projectValue(data: StorableRecordData, field: FieldDeclaration): SqlValue {
|
|
99
|
+
if (!Object.prototype.hasOwnProperty.call(data, field.name)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const value = data[field.name];
|
|
103
|
+
if (value === null) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
switch (typeof value) {
|
|
107
|
+
case 'boolean':
|
|
108
|
+
return value ? 1 : 0;
|
|
109
|
+
case 'number':
|
|
110
|
+
return Number.isFinite(value) ? value : null;
|
|
111
|
+
case 'string':
|
|
112
|
+
return value;
|
|
113
|
+
default:
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function projectedValues(schema: CollectionSchema, data: StorableRecordData): SqlValue[] {
|
|
119
|
+
return (schema.fields ?? []).map(field => projectValue(data, field));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function projectedColumnNames(schema: CollectionSchema): string[] {
|
|
123
|
+
return (schema.fields ?? []).map(field => projectedColumn(field.name));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Serialize on the way in, so a caller mutating its object cannot change
|
|
127
|
+
* what is stored (FR-022). */
|
|
128
|
+
export function encodePayload(data: StorableRecordData): string {
|
|
129
|
+
return JSON.stringify(data ?? {});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type CollectionRow = {
|
|
133
|
+
id: string;
|
|
134
|
+
payload: string;
|
|
135
|
+
created_at?: number | null;
|
|
136
|
+
updated_at?: number | null;
|
|
137
|
+
deleted_at?: number | null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Rebuild a record from the payload column and nothing else.
|
|
142
|
+
*
|
|
143
|
+
* The timestamp and soft-delete fields are surfaced only when the collection
|
|
144
|
+
* declared them, so a consumer cannot come to depend on a field the schema
|
|
145
|
+
* never promised.
|
|
146
|
+
*/
|
|
147
|
+
export function decodeRow<T extends StorableRecordData>(
|
|
148
|
+
schema: CollectionSchema,
|
|
149
|
+
row: Record<string, unknown>,
|
|
150
|
+
): StoredRecord<T> {
|
|
151
|
+
const record: StoredRecord<T> = {
|
|
152
|
+
id: text(row.id),
|
|
153
|
+
data: JSON.parse(text(row.payload)) as T,
|
|
154
|
+
};
|
|
155
|
+
if (hasTimestamps(schema)) {
|
|
156
|
+
if (row.created_at != null) {
|
|
157
|
+
record.createdAt = num(row.created_at);
|
|
158
|
+
}
|
|
159
|
+
if (row.updated_at != null) {
|
|
160
|
+
record.updatedAt = num(row.updated_at);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (hasSoftDelete(schema)) {
|
|
164
|
+
record.deletedAt = row.deleted_at == null ? null : num(row.deleted_at);
|
|
165
|
+
}
|
|
166
|
+
return record;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type WriteColumns = {
|
|
170
|
+
columns: string[];
|
|
171
|
+
values: SqlValue[];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export function insertColumns(
|
|
175
|
+
schema: CollectionSchema,
|
|
176
|
+
id: string,
|
|
177
|
+
data: StorableRecordData,
|
|
178
|
+
now: number,
|
|
179
|
+
): WriteColumns {
|
|
180
|
+
const columns = ['id', 'payload'];
|
|
181
|
+
const values: SqlValue[] = [id, encodePayload(data)];
|
|
182
|
+
|
|
183
|
+
if (hasTimestamps(schema)) {
|
|
184
|
+
columns.push('created_at', 'updated_at');
|
|
185
|
+
values.push(now, now);
|
|
186
|
+
}
|
|
187
|
+
if (hasSoftDelete(schema)) {
|
|
188
|
+
columns.push('deleted_at');
|
|
189
|
+
values.push(null);
|
|
190
|
+
}
|
|
191
|
+
const projected = projectedValues(schema, data);
|
|
192
|
+
for (const [index, name] of projectedColumnNames(schema).entries()) {
|
|
193
|
+
columns.push(name);
|
|
194
|
+
values.push(projected[index]!);
|
|
195
|
+
}
|
|
196
|
+
return { columns, values };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function updateAssignments(
|
|
200
|
+
schema: CollectionSchema,
|
|
201
|
+
data: StorableRecordData,
|
|
202
|
+
now: number,
|
|
203
|
+
): WriteColumns {
|
|
204
|
+
const columns = ['payload'];
|
|
205
|
+
const values: SqlValue[] = [encodePayload(data)];
|
|
206
|
+
|
|
207
|
+
if (hasTimestamps(schema)) {
|
|
208
|
+
columns.push('updated_at');
|
|
209
|
+
values.push(now);
|
|
210
|
+
}
|
|
211
|
+
const projected = projectedValues(schema, data);
|
|
212
|
+
for (const [index, name] of projectedColumnNames(schema).entries()) {
|
|
213
|
+
columns.push(name);
|
|
214
|
+
values.push(projected[index]!);
|
|
215
|
+
}
|
|
216
|
+
return { columns, values };
|
|
217
|
+
}
|