@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +122 -52
  2. package/README.md +663 -661
  3. package/package.json +59 -62
  4. package/skills/sdk-rn-local-db/SKILL.md +0 -1
  5. package/src/adapter/budgets.ts +67 -67
  6. package/src/adapter/collection.ts +303 -298
  7. package/src/adapter/deletion.ts +252 -238
  8. package/src/adapter/index.ts +64 -68
  9. package/src/adapter/lifecycle.ts +517 -444
  10. package/src/adapter/transaction.ts +71 -65
  11. package/src/artifacts/availability.ts +67 -69
  12. package/src/artifacts/cascade.ts +114 -78
  13. package/src/artifacts/paths.ts +29 -29
  14. package/src/artifacts/reconcile.ts +63 -63
  15. package/src/artifacts/store.ts +334 -323
  16. package/src/contract/adapter.ts +76 -78
  17. package/src/contract/attachment.ts +75 -72
  18. package/src/contract/backup.ts +28 -28
  19. package/src/contract/config.ts +54 -54
  20. package/src/contract/encryption.ts +34 -34
  21. package/src/contract/errors.ts +76 -76
  22. package/src/contract/migration.ts +42 -40
  23. package/src/contract/query.ts +81 -81
  24. package/src/contract/record.ts +46 -42
  25. package/src/contract/schema.ts +36 -36
  26. package/src/contract/scope.ts +36 -36
  27. package/src/contract/transaction.ts +52 -46
  28. package/src/crypto/chunked-cipher.ts +153 -144
  29. package/src/crypto/derive.ts +64 -63
  30. package/src/crypto/key-provider.ts +78 -78
  31. package/src/crypto/provider.ts +81 -81
  32. package/src/diagnostics/dev-log.ts +112 -108
  33. package/src/diagnostics/errors.ts +62 -66
  34. package/src/diagnostics/faults.ts +58 -64
  35. package/src/diagnostics/map-native.ts +108 -106
  36. package/src/diagnostics/redact.ts +137 -141
  37. package/src/index.ts +80 -110
  38. package/src/migration/executor.ts +132 -136
  39. package/src/migration/ledger.ts +95 -97
  40. package/src/migration/validate-chain.ts +65 -65
  41. package/src/platform/backup-android-verify.ts +88 -88
  42. package/src/platform/backup-ios.ts +48 -48
  43. package/src/platform/bytes.ts +144 -0
  44. package/src/platform/fs.ts +89 -89
  45. package/src/platform/location.ts +135 -135
  46. package/src/query/budgets.ts +54 -57
  47. package/src/query/compare.ts +91 -91
  48. package/src/query/cursor.ts +100 -99
  49. package/src/query/filter.ts +82 -82
  50. package/src/query/paginate.ts +51 -51
  51. package/src/query/sort.ts +55 -56
  52. package/src/query/validate.ts +171 -171
  53. package/src/schema/declare.ts +42 -45
  54. package/src/schema/validate-config.ts +158 -158
  55. package/src/scope/hash.ts +131 -134
  56. package/src/scope/normalize.ts +83 -90
  57. package/src/scope/validate.ts +83 -83
  58. package/src/sqlite/adapter.ts +26 -26
  59. package/src/sqlite/batch.ts +242 -232
  60. package/src/sqlite/catalog.ts +105 -108
  61. package/src/sqlite/connection.ts +227 -205
  62. package/src/sqlite/index-coverage.ts +54 -53
  63. package/src/sqlite/native.ts +83 -83
  64. package/src/sqlite/row-codec.ts +217 -201
  65. package/src/sqlite/sql-builder.ts +244 -244
  66. package/src/sqlite/transaction-queue.ts +87 -0
  67. package/src/sqlite/transactions.ts +33 -33
  68. package/src/sqlite/values.ts +24 -24
  69. package/src/testing/conformance/attachments.ts +138 -136
  70. package/src/testing/conformance/encryption.ts +150 -150
  71. package/src/testing/conformance/index.ts +161 -161
  72. package/src/testing/conformance/lifecycle.ts +99 -99
  73. package/src/testing/conformance/migrations.ts +177 -177
  74. package/src/testing/conformance/queries.ts +204 -167
  75. package/src/testing/conformance/records.ts +169 -169
  76. package/src/testing/conformance/scopes.ts +86 -88
  77. package/src/testing/conformance/transactions.ts +149 -149
  78. package/src/testing/faults.ts +21 -21
  79. package/src/testing/index.ts +17 -21
  80. package/src/testing/memory-adapter/collection.ts +214 -219
  81. package/src/testing/memory-adapter/index.ts +510 -388
  82. package/src/testing/memory-adapter/migrations.ts +117 -119
  83. package/src/testing/memory-adapter/reset.ts +50 -50
  84. package/src/testing/memory-adapter/store.ts +128 -134
@@ -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
- field => `"${projectedColumn(field.name)}" ${affinityOf(field)}`,
46
- ),
47
- ];
48
- return `CREATE TABLE IF NOT EXISTS "${collectionTable(schema.name)}" (${columns.join(', ')})`;
49
- }
50
-
51
- export function addColumnSql(schema: CollectionSchema, field: FieldDeclaration): string {
52
- return `ALTER TABLE "${collectionTable(schema.name)}" ADD COLUMN "${projectedColumn(
53
- field.name,
54
- )}" ${affinityOf(field)}`;
55
- }
56
-
57
- export function createIndexSql(schema: CollectionSchema): string[] {
58
- const table = collectionTable(schema.name);
59
- const statements = (schema.indexes ?? []).map(index => {
60
- const columns = index.fields.map(field => `"${projectedColumn(field)}"`).join(', ');
61
- return `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${
62
- index.name
63
- }" ON "${table}" (${columns})`;
64
- });
65
- if (hasSoftDelete(schema)) {
66
- // Every default query carries `deleted_at IS NULL`, so without this the
67
- // exclusion turns the cheapest possible filter into a full scan.
68
- statements.push(
69
- `CREATE INDEX IF NOT EXISTS "${table}_live" ON "${table}" (deleted_at)`,
70
- );
71
- }
72
- return statements;
73
- }
74
-
75
- /**
76
- * Project a declared field for the WHERE and ORDER BY columns.
77
- *
78
- * Anything that is not a comparable primitive projects to NULL. A nested
79
- * object has no defensible ordering, and inventing one would make a filter
80
- * silently match records a consumer never meant to select.
81
- */
82
- export function projectValue(data: StorableRecordData, field: FieldDeclaration): SqlValue {
83
- if (!Object.prototype.hasOwnProperty.call(data, field.name)) {
84
- return null;
85
- }
86
- const value = data[field.name];
87
- if (value === null) {
88
- return null;
89
- }
90
- switch (typeof value) {
91
- case 'boolean':
92
- return value ? 1 : 0;
93
- case 'number':
94
- return Number.isFinite(value) ? value : null;
95
- case 'string':
96
- return value;
97
- default:
98
- return null;
99
- }
100
- }
101
-
102
- export function projectedValues(schema: CollectionSchema, data: StorableRecordData): SqlValue[] {
103
- return (schema.fields ?? []).map(field => projectValue(data, field));
104
- }
105
-
106
- export function projectedColumnNames(schema: CollectionSchema): string[] {
107
- return (schema.fields ?? []).map(field => projectedColumn(field.name));
108
- }
109
-
110
- /** Serialize on the way in, so a caller mutating its object cannot change
111
- * what is stored (FR-022). */
112
- export function encodePayload(data: StorableRecordData): string {
113
- return JSON.stringify(data ?? {});
114
- }
115
-
116
- export type CollectionRow = {
117
- id: string;
118
- payload: string;
119
- created_at?: number | null;
120
- updated_at?: number | null;
121
- deleted_at?: number | null;
122
- };
123
-
124
- /**
125
- * Rebuild a record from the payload column and nothing else.
126
- *
127
- * The timestamp and soft-delete fields are surfaced only when the collection
128
- * declared them, so a consumer cannot come to depend on a field the schema
129
- * never promised.
130
- */
131
- export function decodeRow<T extends StorableRecordData>(
132
- schema: CollectionSchema,
133
- row: Record<string, unknown>,
134
- ): StoredRecord<T> {
135
- const record: StoredRecord<T> = {
136
- id: text(row.id),
137
- data: JSON.parse(text(row.payload)) as T,
138
- };
139
- if (hasTimestamps(schema)) {
140
- if (row.created_at != null) {
141
- record.createdAt = num(row.created_at);
142
- }
143
- if (row.updated_at != null) {
144
- record.updatedAt = num(row.updated_at);
145
- }
146
- }
147
- if (hasSoftDelete(schema)) {
148
- record.deletedAt = row.deleted_at == null ? null : num(row.deleted_at);
149
- }
150
- return record;
151
- }
152
-
153
- export type WriteColumns = {
154
- columns: string[];
155
- values: SqlValue[];
156
- };
157
-
158
- export function insertColumns(
159
- schema: CollectionSchema,
160
- id: string,
161
- data: StorableRecordData,
162
- now: number,
163
- ): WriteColumns {
164
- const columns = ['id', 'payload'];
165
- const values: SqlValue[] = [id, encodePayload(data)];
166
-
167
- if (hasTimestamps(schema)) {
168
- columns.push('created_at', 'updated_at');
169
- values.push(now, now);
170
- }
171
- if (hasSoftDelete(schema)) {
172
- columns.push('deleted_at');
173
- values.push(null);
174
- }
175
- const projected = projectedValues(schema, data);
176
- for (const [index, name] of projectedColumnNames(schema).entries()) {
177
- columns.push(name);
178
- values.push(projected[index]!);
179
- }
180
- return { columns, values };
181
- }
182
-
183
- export function updateAssignments(
184
- schema: CollectionSchema,
185
- data: StorableRecordData,
186
- now: number,
187
- ): WriteColumns {
188
- const columns = ['payload'];
189
- const values: SqlValue[] = [encodePayload(data)];
190
-
191
- if (hasTimestamps(schema)) {
192
- columns.push('updated_at');
193
- values.push(now);
194
- }
195
- const projected = projectedValues(schema, data);
196
- for (const [index, name] of projectedColumnNames(schema).entries()) {
197
- columns.push(name);
198
- values.push(projected[index]!);
199
- }
200
- return { columns, values };
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
+ }