@eltjs/elt-sqlite 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +248 -2
- package/dist/computed-dataset.d.ts +22 -0
- package/dist/computed-dataset.js +230 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/open-destination.d.ts +3 -0
- package/dist/open-destination.js +14 -0
- package/dist/reserved.d.ts +7 -0
- package/dist/reserved.js +9 -0
- package/dist/sqlite-append-writer.d.ts +8 -0
- package/dist/sqlite-append-writer.js +10 -0
- package/dist/sqlite-catalog.d.ts +3 -0
- package/dist/sqlite-catalog.js +33 -0
- package/dist/sqlite-checkpoint-store.d.ts +9 -0
- package/dist/sqlite-checkpoint-store.js +84 -0
- package/dist/sqlite-column.d.ts +53 -0
- package/dist/sqlite-column.js +306 -0
- package/dist/sqlite-columns.d.ts +14 -0
- package/dist/sqlite-columns.js +118 -0
- package/dist/sqlite-deduplicating-writer.d.ts +21 -0
- package/dist/sqlite-deduplicating-writer.js +124 -0
- package/dist/sqlite-descriptions.d.ts +8 -0
- package/dist/sqlite-descriptions.js +37 -0
- package/dist/sqlite-destination.d.ts +19 -0
- package/dist/sqlite-destination.js +67 -0
- package/dist/sqlite-file-store.d.ts +17 -0
- package/dist/sqlite-file-store.js +82 -0
- package/dist/sqlite-identifiers.d.ts +8 -0
- package/dist/sqlite-identifiers.js +11 -0
- package/dist/sqlite-overwrite-writer.d.ts +9 -0
- package/dist/sqlite-overwrite-writer.js +13 -0
- package/dist/sqlite-passes.d.ts +39 -0
- package/dist/sqlite-passes.js +174 -0
- package/dist/sqlite-sync-history-schema.d.ts +8 -0
- package/dist/sqlite-sync-history-schema.js +94 -0
- package/dist/sqlite-sync-history.d.ts +12 -0
- package/dist/sqlite-sync-history.js +119 -0
- package/dist/sqlite-table.d.ts +15 -0
- package/dist/sqlite-table.js +83 -0
- package/dist/sqlite-views.d.ts +16 -0
- package/dist/sqlite-views.js +62 -0
- package/dist/sqlite-writer.d.ts +38 -0
- package/dist/sqlite-writer.js +483 -0
- package/dist/view.d.ts +20 -0
- package/dist/view.js +59 -0
- package/package.json +52 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 ezzabuzaid
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,249 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @eltjs/elt-sqlite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The SQLite destination of elt. It keeps the stream of each copy as a raw table in one SQLite file, with a reader view over it and a catalog that describes each table, view and column. A reader queries the file with SQL, for example with `sqlite3 -readonly`. The SQLite destination also has two kinds of derived dataset: a `View`, which SQLite computes on each read, and a `ComputedDataset`, which a rule computes on write. A dbt model has the same two kinds: it is materialized as a view or as a table.
|
|
4
|
+
|
|
5
|
+
This README tells how the SQLite destination keeps columns, commits, checkpoints and the sync history, and how it keeps derived datasets. The pipeline itself is in [elt](../../elt/README.md).
|
|
6
|
+
|
|
7
|
+
The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md), and the glossary of [elt](../../elt/CONTEXT.md) for the words of all of elt.
|
|
8
|
+
|
|
9
|
+
## Columns
|
|
10
|
+
|
|
11
|
+
`sqlite.table(name)` infers the columns from the JSON Schema of the stream. `sqlite.table(name, (columns) => [...])` declares them. `SQLiteColumns.fromSchema(schema)` gives the inferred columns, so a declaration can add to them.
|
|
12
|
+
|
|
13
|
+
Inferred columns map flat JSON Schema fields:
|
|
14
|
+
|
|
15
|
+
| JSON Schema | SQLite |
|
|
16
|
+
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `string` | `TEXT` |
|
|
18
|
+
| `integer` | `INTEGER` |
|
|
19
|
+
| `number` | `REAL` |
|
|
20
|
+
| `boolean` | `INTEGER` with a 0/1 `CHECK` |
|
|
21
|
+
| `array` | `TEXT` that holds a JSON array, with `CHECK (json_valid(...) AND json_type(...) = 'array')` |
|
|
22
|
+
| `string` + `format: 'int64'` | `INTEGER` |
|
|
23
|
+
| `string` + `contentEncoding: 'base64'` | `BLOB` |
|
|
24
|
+
| `string` + `format: 'decimal'` | `TEXT` |
|
|
25
|
+
| `string` + `date`, `date-time`, `date-time-local`, `time-local` | `TEXT`, with a `CHECK` that keeps the canonical form and its width |
|
|
26
|
+
|
|
27
|
+
- Read an array with `json_each`. An array of objects is a JSON array of objects; read a field of each with `json_extract(value, '$.field')`.
|
|
28
|
+
- The `data_type` of the catalog names each [string format](../../elt/README.md#string-formats) with its precision and scale, such as `timestamp(7)` or `decimal(19,4)`. A millisecond `date-time` stays `timestamp`.
|
|
29
|
+
- Nullable scalars and arrays are possible for normal fields. A missing optional field becomes SQL `NULL`.
|
|
30
|
+
- Explicit columns are `text`, `integer`, `int64`, `real`, `boolean`, `date`, `timestamp`, `localTimestamp(precision)` and `blob`. `int64` and `localTimestamp` keep the `int64` and `date-time-local` formats as an inferred column keeps them: the same value, `CHECK` and catalog `data_type`. A local timestamp has no default precision, so you give it. They permit null unless you mark them `.notNull()` or `.primaryKey()`. A missing or undefined explicit field fails. An explicit projection can leave out nested fields that SQLite cannot hold.
|
|
31
|
+
- Table and column names follow the [naming rule](../../elt/README.md#names). SQLite compares names without ASCII case, and it has no length limit.
|
|
32
|
+
|
|
33
|
+
Tables:
|
|
34
|
+
|
|
35
|
+
- SQLite makes strict tables. SQL constraints that exist stay in force.
|
|
36
|
+
- Each copy adds `loaded_at`, the UTC start time of the load. A field with that name gets a different column name.
|
|
37
|
+
- Deduplication upserts on a unique index that is named after the table and the key (`_elt_dedup_<hash>`). SQLite makes the index one time, and makes it again only when the key changes. It uses the native [UPSERT with a cursor comparison](https://www.sqlite.org/lang_upsert.html).
|
|
38
|
+
- Deduplication also checks the stored types of the key and cursor columns, and refuses null keys and cursors.
|
|
39
|
+
- A change to a normal `append` or `overwrite` removes the index of the mode. Explicit constraints stay.
|
|
40
|
+
- A table of append history with repeated keys must be replaced with `overwrite_dedup` before incremental deduplication can start.
|
|
41
|
+
- When the stored definition of a table does not agree with the columns of the stream, the load [evolves it](../../elt/README.md#shape-changes) before it reads. SQLite cannot change the type or the constraints of a column in place. Thus, in one transaction, the rows move, column by name, into a table of the new definition. That table takes the name of the table, with its index and its reader view. A value that its type or a `CHECK` refuses fails the load, and the table stays as it was.
|
|
42
|
+
- An overwrite reloads such a table into a hidden table `_elt_next_<hash>`. The hidden table replaces the table and its reader view when the stream ends ([resets](../../elt/README.md#resets)).
|
|
43
|
+
- The writer of each target is in the reserved table `_elt_writers` ([target ownership](../../elt/README.md#target-ownership)).
|
|
44
|
+
|
|
45
|
+
Counts:
|
|
46
|
+
|
|
47
|
+
- The `count` of a committed copy is the number of accepted input observations, also deduplication no-ops.
|
|
48
|
+
- `deleted` is the number of accepted deletions, also keys that were not there.
|
|
49
|
+
- Neither count is the final number of rows.
|
|
50
|
+
|
|
51
|
+
## File bytes in the database
|
|
52
|
+
|
|
53
|
+
`c.blob('bytes').from(notes.attachments.file)` keeps the original bytes of a file in the SQLite file, not in a [file storage](../../elt/README.md#local-files). It does not depend on `.store(files)`. The file and its row then commit together. See [stored files](../../elt/README.md#stored-files-and-document-parsing) in elt for the declarations.
|
|
54
|
+
|
|
55
|
+
SQLite stores an original file in chunks, because one BLOB has a limit of 1,000,000,000 bytes (`SQLITE_MAX_LENGTH` in the build of Node), and a value of the full file would stay in memory.
|
|
56
|
+
|
|
57
|
+
- The `bytes` column holds an `INTEGER` file id.
|
|
58
|
+
- A table `_elt_files_<hash>` for each column holds `(file, n, bytes)` rows of up to 4 MiB. `n` counts from 0.
|
|
59
|
+
- The hash is the first 40 hex characters of the SHA-256 of `JSON.stringify([table, column])`, with the two names in ASCII lower case. Thus table `a_b` with column `c` and table `a` with column `b_c` keep their chunks apart.
|
|
60
|
+
- The description of the column names its chunk table.
|
|
61
|
+
- An empty file has one empty chunk. A file that is not available gives a null value.
|
|
62
|
+
- Triggers remove the chunks of a row in the same transaction when the row is deleted, overwritten or replaced. A record that a deduplication guard refuses stores no chunks.
|
|
63
|
+
|
|
64
|
+
Read a file back in order, with the chunk table that the description names:
|
|
65
|
+
|
|
66
|
+
```sql
|
|
67
|
+
SELECT c.bytes FROM "_elt_files_<hash>" AS c
|
|
68
|
+
WHERE c.file = (SELECT bytes FROM attachments WHERE id = ?)
|
|
69
|
+
ORDER BY c.n;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Checked on 2026-09-24 (macOS 26.6.2, Node.js 26.8.1): a file of 1.5 GB loaded as 358 chunks in 3.3 s, with a peak RSS of 137 MiB. The chunks put together again had the SHA-256 of the file.
|
|
73
|
+
|
|
74
|
+
## Locks and commits
|
|
75
|
+
|
|
76
|
+
- The SQLite file has a [persistent WAL](./CONTEXT.md). Each connection that writes the file opens it with `openDestination(path)`, which gives the journal `PersistentWriteAheadLog` of `@zukhruf/sqlite-open`. The last connection that writes the file and closes decides if the `-wal` and `-shm` files stay, so all of these connections use it. Use `openDestination` also in a host that opens the file to write.
|
|
77
|
+
- Readers never wait for a commit, and a commit never waits for readers.
|
|
78
|
+
- The `-wal` and `-shm` files stay after the last connection closes. Thus `sqlite3 -readonly` reads the file also from a folder that it cannot write, for example the folder of an agent in a sandbox.
|
|
79
|
+
- A connection that stops during a commit leaves only the committed rows. The next reader reads them at once, with no recovery step.
|
|
80
|
+
- A connection waits up to 30 seconds for the commit of a different connection.
|
|
81
|
+
- SQLite takes no lock other than its transactions. Loads into one file run side by side. A [checkpoint store](#checkpoints) keeps two runs of one copy apart.
|
|
82
|
+
- No transaction stays open between commits, as in Postgres.
|
|
83
|
+
- `prepare` only refuses and stages. It refuses the target of a different writer, a reader view that is not its own, and stored rows that a deduplicating load cannot keep.
|
|
84
|
+
- Each commit is one write transaction. The first commit also makes the table, the index of its mode, its reader view, its descriptions and its writer.
|
|
85
|
+
- Other writers of the file wait only for a commit. A stream that fails before its first commit leaves nothing.
|
|
86
|
+
- Each stream stages its operations in a `TEMP` table that only its connection sees. The commit of a different stream never publishes them, and a crash leaves nothing.
|
|
87
|
+
- A commit merges the staged operations of one stream into its table. The result is the same as when the operations apply one at a time: a staged `DELETE` removes its key, only the records after the last `DELETE` of a key count, `replace` keeps the last record, and `cursor_newer` keeps the first record with the greatest cursor.
|
|
88
|
+
- Original files stream into a `TEMP` chunk table that only the connection sees, and they get to the store in the commit of their rows. After each merge, SQLite deletes the chunks that no row refers to. A clear empties the store.
|
|
89
|
+
- A file column that the target drops takes its chunk table with it when the table evolves or a reload replaces it.
|
|
90
|
+
- `:memory:` is possible only for a full refresh. The database of the run is gone when its handle closes, so it cannot keep incremental progress.
|
|
91
|
+
|
|
92
|
+
## Checkpoints
|
|
93
|
+
|
|
94
|
+
`SQLiteCheckpointStore({ path })` keeps the checkpoints of incremental copies in a table `checkpoints` (`id`, `binding`, `reloading`, `state`) in its own file. Use it with SQLite destinations.
|
|
95
|
+
|
|
96
|
+
- The checkpoint file and its `-wal` and `-shm` files are owner-only (`0600`). The checkpoint file has a write-ahead log, and SQLite deletes its `-wal` and `-shm` files when the last connection closes.
|
|
97
|
+
- Use a file that is not a SQLite destination file. Its parent folder must exist. `:memory:` is refused.
|
|
98
|
+
- The store holds one lock for each copy `id` through `SqliteStore` of `@zukhruf/mutex/sqlite`, in a `<path>.locks` folder beside the file. Different IDs run in parallel. A run that finds one ID in use releases the IDs that it took and fails with `Checkpoint <id> is in use by another run`. The kernel releases a lock when its process stops. See [ADR 0005](./docs/adr/0005-each-checkpointed-copy-has-its-own-lock.md).
|
|
99
|
+
- Each save commits by itself, so no checkpoint transaction stays open while the load runs.
|
|
100
|
+
- A `checkpoints` table that a different version declared in a different way is replaced with its checkpoints, so each copy starts again.
|
|
101
|
+
|
|
102
|
+
## Sync history
|
|
103
|
+
|
|
104
|
+
`SQLiteSyncHistory()` records each pass in the SQLite file that its connection loads, beside the data. See [sync history](../../elt/README.md#sync-history) in elt.
|
|
105
|
+
|
|
106
|
+
- It records only connections with a SQLite destination, and it refuses other connections when the pipeline validates. The destination must be a file, not `:memory:`.
|
|
107
|
+
- Its tables are `_elt_sync_attempts`, `_elt_extraction_coverage` and `_elt_stream_checks`. The last one holds the latest check of each stream by a watch that found it unchanged.
|
|
108
|
+
- `install()` makes those tables and publishes the views `sync_attempts`, `extraction_coverage`, `sync_status` and `stream_status`, and the [catalog](#the-catalog), in each file. The pipeline calls it before each run and watch. Tables that a different version declared in a different way are replaced, with their history.
|
|
109
|
+
- `new SQLiteSyncHistory({ soleWriter: true })` is for a host that knows that no other process runs a pass of its connections, such as the leader of the flight of a connection. Its install records as `failed` each pass of those connections that started and never ended, because the process that ran it ended first. Airbyte also fails the unfinished jobs of a connection before it starts the next. With no `soleWriter`, such a pass stays `running`, because a different process can still run it. See [ADR 0006](./docs/adr/0006-a-sole-writer-fails-the-passes-it-finds-unfinished.md).
|
|
110
|
+
- `target_schema` is `main` in SQLite.
|
|
111
|
+
- `readPassStatus(path)` gives the latest pass of a SQLite file and the latest outcome of each stream. When several connections load the file, their latest passes show as one pass. It is `running` when one connection runs. Else it is `cancelled` when one pass stopped, `succeeded` when all passes succeeded, `failed` when all passes failed, and `partial` in all other cases. It starts with the first pass and completes with the last. Its error has the error of each pass that did not succeed, one on each line. Its failure type is `config` when one pass has a `config` failure. Its last success and its `current_as_of` are the earliest of the passes, or null when one pass has no value. When no file is at the path, it gives no pass and no streams. See [ADR 0008](./docs/adr/0008-a-file-that-several-connections-load-shows-one-pass.md).
|
|
112
|
+
|
|
113
|
+
## Reserved names
|
|
114
|
+
|
|
115
|
+
The destination and its sync history own each table, index and view whose name starts with `_elt_`, in any case. Examples are `_elt_writers`, `_elt_descriptions`, the chunk tables `_elt_files_<hash>` and the sync history's `_elt_sync_attempts`. A stream, a view or a computed dataset never has such a name, and `publishSQLiteViews` refuses one. A reader that follows the data of a file, such as a changefeed, skips these tables with `isReserved(name)`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { isReserved } from '@eltjs/elt-sqlite';
|
|
119
|
+
|
|
120
|
+
for (const event of batch) if (!isReserved(event.table)) await accept(event);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## The catalog
|
|
124
|
+
|
|
125
|
+
The view `catalog` lists each table, view and column of the SQLite file that readers query, with its description. The sync history publishes it in each file that its connections load. `installSQLiteCatalog({ path })` publishes it in a file that no pipeline loads.
|
|
126
|
+
|
|
127
|
+
## Declare a view
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { View } from '@eltjs/elt-sqlite';
|
|
131
|
+
|
|
132
|
+
const unread = new View({
|
|
133
|
+
into: sqlite,
|
|
134
|
+
name: 'unread_count',
|
|
135
|
+
description: 'How many messages each chat has that the user did not read.',
|
|
136
|
+
columns: {
|
|
137
|
+
chat: 'The chat.',
|
|
138
|
+
n: 'Number of messages that the user did not read.',
|
|
139
|
+
},
|
|
140
|
+
sql: 'SELECT chat, count(*) AS n FROM messages WHERE read = 0 GROUP BY chat',
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Give the view to the pipeline in `derived`. `sqlite` is the `SQLiteDestination` of the copies.
|
|
145
|
+
|
|
146
|
+
- A view names no inputs. Its SQL reads them. See [ADR 0001](./docs/adr/0001-a-view-names-no-inputs.md).
|
|
147
|
+
- The view appears when its query compiles. Thus a reader never finds a view that fails.
|
|
148
|
+
- While the SQL reads a raw table or a reader view that a copy of the pipeline has not made yet, the view waits. It appears at the refresh after that copy loads.
|
|
149
|
+
- While the SQL reads a view that comes before it in `derived` and that still waits, the view waits too, as dbt skips a model whose upstream model was skipped. Both appear at the refresh after the copy loads.
|
|
150
|
+
- Any other error of the query, for example a misspelled table, is a dataset failure. It fails the run with the error of SQLite.
|
|
151
|
+
- The columns must be the same as the columns that the query gives, and the catalog lists them with their descriptions.
|
|
152
|
+
- A view that comes after a computed dataset in `derived` can read the view of that dataset.
|
|
153
|
+
- A refresh that changes nothing does not change the schema of the SQLite file.
|
|
154
|
+
|
|
155
|
+
## Declare a computed dataset
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { DocumentText } from '@eltjs/elt';
|
|
159
|
+
import { ComputedDataset } from '@eltjs/elt-sqlite';
|
|
160
|
+
|
|
161
|
+
const text = new ComputedDataset({
|
|
162
|
+
into: sqlite,
|
|
163
|
+
name: 'document_text',
|
|
164
|
+
description: 'The text of each stored document.',
|
|
165
|
+
from: [documents],
|
|
166
|
+
compute: new DocumentText({
|
|
167
|
+
parser: () => parser,
|
|
168
|
+
files: files.directory,
|
|
169
|
+
column: 'documentRef',
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`documents` is the target of a copy, and `from` can name more than one. The rule reads each row that those copies loaded. See [Write a rule](../../elt/README.md#write-a-rule) in elt.
|
|
175
|
+
|
|
176
|
+
`from` can also name a `View` that comes before the computed dataset in `derived`. Then the rule reads each row that the view gives, as a dbt model reads the model that it refs. For example, a view keeps the latest version of each record, and the computed dataset reads only those rows. While the view waits for its tables, the computed dataset has no input from it.
|
|
177
|
+
|
|
178
|
+
A computed dataset keeps one output row for each key, in the table `raw_<name>`. Readers query the view `<name>`:
|
|
179
|
+
|
|
180
|
+
| Column | Holds |
|
|
181
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| the key | the key of the input, with the name that the rule gives, for example `ref` |
|
|
183
|
+
| `input_hash` | the SHA-256 of the input |
|
|
184
|
+
| `rule` | the rule identity, for example the parser of the format. `unsupported` when the rule does not handle the input |
|
|
185
|
+
| `status` | `computed`, `empty` or `failed` |
|
|
186
|
+
| `error` | why the rule failed, or `NULL` |
|
|
187
|
+
| rule columns | the columns of the rule, for example `content` |
|
|
188
|
+
|
|
189
|
+
At each refresh, the computed dataset compares the inputs with the output rows. See [ADR 0002](./docs/adr/0002-a-computed-dataset-keeps-one-output-row-for-each-key.md):
|
|
190
|
+
|
|
191
|
+
- It computes each key that is new, or whose input or rule identity changed. The refresh after the passes also computes each key whose status is `failed`, once for each pass ([ADR 0007](./docs/adr/0007-each-pass-tries-an-input-at-most-once.md)).
|
|
192
|
+
- It deletes each output row whose key no input has any more.
|
|
193
|
+
- It keeps an output row when the rule stops handling its input.
|
|
194
|
+
- Each call of the rule commits as it settles, so readers see the output rows as they land. See [ADR 0003](./docs/adr/0003-each-rule-call-commits-as-it-settles.md).
|
|
195
|
+
- When the declaration of the raw table changes, the refresh makes the table again. See [ADR 0004](./docs/adr/0004-a-changed-declaration-makes-the-table-again.md).
|
|
196
|
+
|
|
197
|
+
## When something fails
|
|
198
|
+
|
|
199
|
+
- **A rule failure** gives an output row with the status `failed` and the reason in `error`. The run continues, and the refresh after the next pass tries the input again.
|
|
200
|
+
- **A dataset failure**, for example a query that SQLite refuses, rejects the run, or it stops the watch.
|
|
201
|
+
- **A copy that failed** keeps its last committed rows. The derived datasets read those rows.
|
|
202
|
+
|
|
203
|
+
## Recipes
|
|
204
|
+
|
|
205
|
+
One use case each, with a full program that you can run:
|
|
206
|
+
|
|
207
|
+
- [Search the text of stored files with SQL](./docs/recipes/search-the-text-of-stored-files.md)
|
|
208
|
+
|
|
209
|
+
## Decisions
|
|
210
|
+
|
|
211
|
+
- [ADR 0001](./docs/adr/0001-a-view-names-no-inputs.md): a view names no inputs.
|
|
212
|
+
- [ADR 0002](./docs/adr/0002-a-computed-dataset-keeps-one-output-row-for-each-key.md): a computed dataset keeps one output row for each key.
|
|
213
|
+
- [ADR 0003](./docs/adr/0003-each-rule-call-commits-as-it-settles.md): each rule call commits as it settles.
|
|
214
|
+
- [ADR 0004](./docs/adr/0004-a-changed-declaration-makes-the-table-again.md): a changed declaration makes the table again.
|
|
215
|
+
- [ADR 0005](./docs/adr/0005-each-checkpointed-copy-has-its-own-lock.md): each checkpointed copy has its own lock.
|
|
216
|
+
- [ADR 0006](./docs/adr/0006-a-sole-writer-fails-the-passes-it-finds-unfinished.md): a sole writer fails the passes that it finds unfinished.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
Run the commands from the workspace root.
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
npx nx run elt-sqlite:test # builds, then runs src/*.test.ts
|
|
224
|
+
npx nx run elt-sqlite:typecheck # formats, lints, then type checks
|
|
225
|
+
npx nx run elt-sqlite:build # compiles src/ to dist/
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The files of the destination:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
src/
|
|
232
|
+
sqlite-destination.ts SQLiteDestination: tables and the load
|
|
233
|
+
open-destination.ts openDestination: the open of each connection that writes the file
|
|
234
|
+
sqlite-writer.ts the stage, the commit, evolve and reload of a table
|
|
235
|
+
sqlite-checkpoint-store.ts SQLiteCheckpointStore
|
|
236
|
+
sqlite-sync-history.ts SQLiteSyncHistory
|
|
237
|
+
sqlite-catalog.ts installSQLiteCatalog
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The files of derived datasets:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
src/
|
|
244
|
+
view.ts View: a view that SQLite computes on each read
|
|
245
|
+
computed-dataset.ts ComputedDataset: one output row for each key
|
|
246
|
+
sqlite-views.ts publishSQLiteViews: makes described views and checks their columns
|
|
247
|
+
view.test.ts the tests of View
|
|
248
|
+
computed-dataset.test.ts the tests of ComputedDataset, and of the refreshes of a run and a watch
|
|
249
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SQLInputValue } from 'node:sqlite';
|
|
2
|
+
import { type Connection, DerivedDataset, type RefreshTrigger, type Rule } from '@eltjs/elt';
|
|
3
|
+
import { SQLiteDestination } from './sqlite-destination.ts';
|
|
4
|
+
import type { SQLiteTable } from './sqlite-table.ts';
|
|
5
|
+
import { View } from './view.ts';
|
|
6
|
+
export declare class ComputedDataset<Input, Output extends SQLInputValue> extends DerivedDataset<SQLiteTable> {
|
|
7
|
+
#private;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
constructor({ into, name, description, from, compute, }: {
|
|
10
|
+
into: SQLiteDestination;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
from: readonly (SQLiteTable | View)[];
|
|
14
|
+
compute: Rule<Input, Output>;
|
|
15
|
+
});
|
|
16
|
+
validate(connections: readonly Connection<SQLiteTable>[]): void;
|
|
17
|
+
refresh({ connections, signal, trigger, }: {
|
|
18
|
+
readonly connections: readonly Connection<SQLiteTable>[];
|
|
19
|
+
readonly signal?: AbortSignal;
|
|
20
|
+
readonly trigger: RefreshTrigger;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
}
|