@eltjs/source-microsoft-sql-server 0.0.0-stage → 0.1.0
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 +216 -2
- package/dist/change-tracking-stream.d.ts +11 -0
- package/dist/change-tracking-stream.js +149 -0
- package/dist/full-refresh-stream.d.ts +12 -0
- package/dist/full-refresh-stream.js +25 -0
- package/dist/rowversion-stream.d.ts +11 -0
- package/dist/rowversion-stream.js +70 -0
- package/dist/snapshot-stream.d.ts +12 -0
- package/dist/snapshot-stream.js +38 -0
- package/dist/sql-server-copies.d.ts +10 -0
- package/dist/sql-server-copies.js +25 -0
- package/dist/sql-server-fields.d.ts +11 -0
- package/dist/sql-server-fields.js +172 -0
- package/dist/sql-server-source.d.ts +32 -0
- package/dist/sql-server-source.js +113 -0
- package/dist/sql-server-stream.d.ts +27 -0
- package/dist/sql-server-stream.js +65 -0
- package/dist/watch-gauges.d.ts +13 -0
- package/dist/watch-gauges.js +132 -0
- package/dist/whole-table-gauges.d.ts +10 -0
- package/dist/whole-table-gauges.js +31 -0
- package/package.json +59 -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,217 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @eltjs/source-microsoft-sql-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An ELT source for one SQL Server database. Each user table that the login can read becomes a stream named `<schema>.<table>`. The source needs SQL Server 2016 or later, Azure SQL Database or Azure SQL Managed Instance.
|
|
4
|
+
|
|
5
|
+
The words in this README have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Connection, Pipeline } from '@eltjs/elt';
|
|
9
|
+
import {
|
|
10
|
+
PostgresCheckpointStore,
|
|
11
|
+
PostgresDestination,
|
|
12
|
+
PostgresSyncHistory,
|
|
13
|
+
} from '@eltjs/elt-postgresql';
|
|
14
|
+
import { SqlServerDatabase } from '@eltjs/sdk-microsoft-sql-server';
|
|
15
|
+
import { sqlServerCopies } from '@eltjs/source-microsoft-sql-server/sql-server-copies';
|
|
16
|
+
import { SqlServerSource } from '@eltjs/source-microsoft-sql-server/sql-server-source';
|
|
17
|
+
|
|
18
|
+
const url = 'postgres://warehouse:warehouse@127.0.0.1:55432/warehouse';
|
|
19
|
+
const schema = 'shop';
|
|
20
|
+
|
|
21
|
+
const database = new SqlServerDatabase(
|
|
22
|
+
'Server=sql.example.com,1433;Database=shop;User Id=reader;Password=...;Encrypt=true',
|
|
23
|
+
);
|
|
24
|
+
const source = await SqlServerSource.discover(database, { schemas: ['dbo'] });
|
|
25
|
+
const destination = new PostgresDestination({ url, schema });
|
|
26
|
+
|
|
27
|
+
await new Pipeline({
|
|
28
|
+
history: new PostgresSyncHistory({ url }),
|
|
29
|
+
connections: [
|
|
30
|
+
new Connection({
|
|
31
|
+
name: 'sql-server',
|
|
32
|
+
source,
|
|
33
|
+
destination,
|
|
34
|
+
checkpoints: new PostgresCheckpointStore({ url, schema }),
|
|
35
|
+
steps: sqlServerCopies(source, (stream) => destination.table(stream)),
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
}).run();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How to use it
|
|
42
|
+
|
|
43
|
+
1. Make a `SqlServerDatabase` from an ADO.NET connection string. [mssql](https://github.com/tediousjs/node-mssql) parses it. It does not read an `mssql://` URL. The connection string must name a `Database`.
|
|
44
|
+
2. Call `SqlServerSource.discover(database, { schemas })`. The streams are the tables of the database, so the source is discovered. A host discovers it again on each run.
|
|
45
|
+
3. Call `sqlServerCopies` to make one copy for each discovered table, with the sync strategy that discovery selected.
|
|
46
|
+
4. Connect the copies to a destination and a checkpoint store through `Connection` and `Pipeline`.
|
|
47
|
+
|
|
48
|
+
`@eltjs/sdk-microsoft-sql-server` owns the database: the connection string, discovery, exact reads, Change Tracking and the values that a watch reads. The source owns the streams, their schemas and the sync strategy. The SDK and the source hold no configuration from environment variables or for a standalone app.
|
|
49
|
+
|
|
50
|
+
`sqlServerCopies` makes each stream a copy:
|
|
51
|
+
|
|
52
|
+
- A stream with a key gets an incremental `append_dedup` copy: Change Tracking, rowversion and snapshot streams.
|
|
53
|
+
- A stream with no key gets a full-refresh `overwrite` copy.
|
|
54
|
+
- The ID of each copy is `<source identity>/<schema>.<table>`, for example `sql-server:shop/dbo.orders`. The source identity is `sql-server:<database>`. Copy IDs must be different across a pipeline. Thus two databases that each have a table with the same name can be in one pipeline. See [ADR 0004](./docs/adr/0004-a-copy-id-carries-the-source-identity.md).
|
|
55
|
+
|
|
56
|
+
A table whose columns changed gets to the pipeline as a stream whose fields changed. The copy keeps its rows and its checkpoint, and the destination changes its table in place (see shape changes in [elt](../../../elt/README.md#shape-changes)). The source fills in a field that the table gained. See the sync strategies.
|
|
57
|
+
|
|
58
|
+
## Access
|
|
59
|
+
|
|
60
|
+
A read-only login is sufficient: `db_datareader`, and `VIEW CHANGE TRACKING` on each tracked table whose changes and deletions it must read. You do not have to enable a feature on the server. The source uses Change Tracking and snapshot isolation where an administrator turned them on. It reads every other table by its rowversion, by snapshot or fully.
|
|
61
|
+
|
|
62
|
+
`VIEW SERVER STATE` (`VIEW DATABASE STATE` on Azure SQL Database) is optional. With it, a watch sees each write to a table that the source reads by snapshot or fully, also to a column with no statistics. See [Watching](#watching).
|
|
63
|
+
|
|
64
|
+
- A connection string that names no `Database` is refused. Thus a read never goes to the default database of the login.
|
|
65
|
+
- A server that cannot be reached fails with `SqlServerUnavailableError`. It is down, it restarts or fails over, its address is wrong, or the network dropped the connection. This is a `system` failure, so a later attempt or the next check of a watch can succeed.
|
|
66
|
+
- A login that the server refuses fails with `SqlServerLoginError`: the credentials are wrong, the login is disabled, or it cannot open the database. This is a `config` failure, which the user must fix. The login errors that SQL Server gives while it fails over or is busy (4060, 10928, 10929, 40197, 40501, 40613) are not refusals: tedious tries them again, and then they are `SqlServerUnavailableError`.
|
|
67
|
+
- Both errors name the server and the database, but not the credentials.
|
|
68
|
+
- A column that the login cannot read (a column-level `DENY`) is left out, and the table's description names it. A primary key with such a column identifies nothing, so its table reads fully.
|
|
69
|
+
- A table with no readable column is not a stream.
|
|
70
|
+
- When a grant is revoked after discovery, only the stream of that table fails, with `SqlServerPermissionError`, which names the grant (`SELECT ON [dbo].[orders]`, `VIEW CHANGE TRACKING ON [dbo].[orders]`).
|
|
71
|
+
- Columns under dynamic data masking load as the login reads them: masked, unless the login has `UNMASK`. Their description says so.
|
|
72
|
+
|
|
73
|
+
## Streams
|
|
74
|
+
|
|
75
|
+
Discovery lists `sys.tables`: user tables. It does not list views, system tables, external tables, or the change tables that CDC keeps in `cdc`.
|
|
76
|
+
|
|
77
|
+
- A host can keep only some schemas (`{ schemas }`). Write each name exactly as `sys.schemas` spells it. A schema in the list that holds no table that the login can read fails discovery. Thus a misspelling, a casing that the database does not use, or a missing grant never loads less without an error.
|
|
78
|
+
- A column of an alias type (`CREATE TYPE ... FROM nvarchar(20)`) reads as the system type that it is built on, also when the login cannot see the alias type.
|
|
79
|
+
- The stream key is the table's primary key in key order, plus the `_type` field of a `sql_variant` key column.
|
|
80
|
+
- Descriptions come from `MS_Description` extended properties. When a column has none, the source makes one from the name and the type of the column. The table's description also tells how the stream reads the table, and which grant would let it read only changes.
|
|
81
|
+
|
|
82
|
+
| SQL Server | Field |
|
|
83
|
+
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `tinyint`, `smallint`, `int` | `integer` |
|
|
85
|
+
| `bigint`, `rowversion` | `int64` |
|
|
86
|
+
| `bit` | `boolean` |
|
|
87
|
+
| `real`, `float` | `number` (the IEEE value, exactly) |
|
|
88
|
+
| `decimal(p, s)`, `numeric(p, s)` | `decimal`, precision `p`, scale `s` |
|
|
89
|
+
| `money`, `smallmoney` | `decimal` (19, 4) and (10, 4) |
|
|
90
|
+
| `date` | `date` |
|
|
91
|
+
| `datetime2(p)`, `datetime`, `smalldatetime` | `date-time-local`, precision `p`, 3 and 0 |
|
|
92
|
+
| `datetimeoffset(p)` | `date-time`, precision `p`: the UTC instant; beside it `<column>_offset`, the offset it was written with in minutes |
|
|
93
|
+
| `time(p)` | `time-local`, precision `p` |
|
|
94
|
+
| `char`, `varchar`, `nchar`, `nvarchar`, `text`, `ntext`, `xml`, `uniqueidentifier`, `json` | `string` (a uniqueidentifier in upper case) |
|
|
95
|
+
| `hierarchyid` | `string`, its path (`/1/2/`) |
|
|
96
|
+
| `geography`, `geometry` | `string`, `SRID=<id>;` then the shape as text with its Z and M (`SRID=4326;POINT (-122.349 47.651 10 3)`) |
|
|
97
|
+
| `sql_variant` | `string`, the value as text; beside it `<column>_type`, its base type |
|
|
98
|
+
| `binary`, `varbinary`, `image`, CLR types | `string`, base64 |
|
|
99
|
+
|
|
100
|
+
The formats are the string formats of [elt](../../../elt/README.md#string-formats). The driver gives `decimal` and `money` as JavaScript numbers and `datetime2` as a millisecond `Date`, and it drops the offset of a `datetimeoffset`. Thus the source selects each such column as the text that SQL Server spells, and decodes it into its canonical form. See [ADR 0002](./docs/adr/0002-each-column-is-read-as-the-text-that-sql-server-spells.md).
|
|
101
|
+
|
|
102
|
+
When a column of the table already has the name `<column>_offset` or `<column>_type`, the added field gets the next free name: `_1`, then `_2`. The description of the column names the field that holds its offset. See [ADR 0003](./docs/adr/0003-an-added-field-takes-the-next-free-name.md).
|
|
103
|
+
|
|
104
|
+
## Sync strategies
|
|
105
|
+
|
|
106
|
+
Discovery selects how each table reads. Each table reads with the first strategy that it can use:
|
|
107
|
+
|
|
108
|
+
| Strategy | When | Copy |
|
|
109
|
+
| --------------- | ---------------------------------------------------------------------- | ----------------------------- |
|
|
110
|
+
| Change Tracking | the table has a key, tracks changes, and the login can see its changes | `incremental`, `append_dedup` |
|
|
111
|
+
| Rowversion | the table has a key and a `rowversion` column | `incremental`, `append_dedup` |
|
|
112
|
+
| Snapshot | all other tables that have a key | `incremental`, `append_dedup` |
|
|
113
|
+
| Full refresh | the table has no key | `full_refresh`, `overwrite` |
|
|
114
|
+
|
|
115
|
+
**Change Tracking** is for a keyed table that tracks changes, when the login has `VIEW CHANGE TRACKING`.
|
|
116
|
+
|
|
117
|
+
- The first read loads every row, 10,000 rows for each checkpoint, in primary key order. It starts from the position in the table's change history that it read before it began. A load that fails continues after its last checkpoint.
|
|
118
|
+
- Later reads apply each key that changed since: its row as it is now, or a `DELETE`.
|
|
119
|
+
- With `ALLOW_SNAPSHOT_ISOLATION` on, the position, the check and the changes come from one short snapshot transaction for each table. Without it, the source reads the position first. Thus a change made during the read is read again the next time, and the source checks the history again after the read.
|
|
120
|
+
|
|
121
|
+
**Rowversion** is for a keyed table with a `rowversion` column.
|
|
122
|
+
|
|
123
|
+
- Each read takes the rows above the last rowversion that it read, and below `MIN_ACTIVE_ROWVERSION()`. Thus a row that a transaction still writes waits for a later read.
|
|
124
|
+
- The source does not see deletions. A deleted row stays loaded.
|
|
125
|
+
- Its state records the columns that it read. A column that the table gained changes no rowversion. Thus the next read starts again from the first row, and keeps what is loaded.
|
|
126
|
+
|
|
127
|
+
**Snapshot** is for a keyed table with no Change Tracking that the login can read and no `rowversion` column. It is a snapshot stream of [elt](../../../elt/CONTEXT.md):
|
|
128
|
+
|
|
129
|
+
- Each read takes every row. The copy compares the rows with the snapshot in its checkpoint, writes each row that is new or changed, and deletes each row whose key the read did not give. The read gives every row, so a key that it does not give was deleted (the `ClosedWorld` deletion policy).
|
|
130
|
+
- A read that fails deletes nothing.
|
|
131
|
+
- A full-refresh copy of the stream replaces its target with every row.
|
|
132
|
+
|
|
133
|
+
**Full refresh** is for a table with no key, or with a key that the login cannot read in full. Each read takes every row, and the copy replaces its target.
|
|
134
|
+
|
|
135
|
+
A position in a table's change history is a Change Tracking version and the table's tracking generation. The generation is the table's `begin_version`, which a `TRUNCATE` moves to the current version, and the creation time of its change table, which enabling tracking again makes new. See [ADR 0001](./docs/adr/0001-a-change-tracking-position-carries-the-tracking-generation.md).
|
|
136
|
+
|
|
137
|
+
- The history no longer covers a saved position when the position is below the table's minimum valid version, ahead of the database's current version, of a different generation, or at the version where its generation began. The stream then sends `RESET` and loads every row again into a hidden target, which replaces the table when the stream ends (see resets in [elt](../../../elt/README.md#resets)).
|
|
138
|
+
- The stream does the same for a column that the table gained since the state was saved, because Change Tracking reports no change for the rows to which it was added.
|
|
139
|
+
- The generation finds the case that the minimum valid version cannot find: a truncate just after a sync, with no change between them, leaves the minimum valid version equal to the saved one.
|
|
140
|
+
- A truncate moves the generation to the current version. When the generation began at the saved version, a truncate leaves it as it was. Thus such a position can show no truncate, and the stream loads every row again.
|
|
141
|
+
|
|
142
|
+
Pages compare the full primary key as a tuple. Each part is cast to the declared type and collation of its column. Thus a page does not skip or repeat rows that have the same leading key value, and a `varchar` key under a SQL collation orders as the column sorts.
|
|
143
|
+
|
|
144
|
+
## Watching
|
|
145
|
+
|
|
146
|
+
The source gives elt's pipeline one change signal for each stream, checked every 30 seconds (elt ADR 0020). The first check opens one session for the watch, and the pipeline closes it when the watch stops. Each sync strategy names the gauges of its stream, and the gauges of one stream are one group. On each check, each collector reads the values of all tables in one query, and the database version gives the Change Tracking version and the last rowversion in one query. A check wakes a stream when one gauge of its group changed. The first check wakes every stream. A check that wakes no stream makes no pass.
|
|
147
|
+
|
|
148
|
+
| Sync strategy | Gauges |
|
|
149
|
+
| ------------------------- | ------------------------------------------------------------------ |
|
|
150
|
+
| Change Tracking | the Change Tracking version, the row count |
|
|
151
|
+
| Rowversion | the last rowversion |
|
|
152
|
+
| Snapshot and full refresh | the statistics, the row count, the last write (optional), the hour |
|
|
153
|
+
|
|
154
|
+
- **The Change Tracking version** (`CHANGE_TRACKING_CURRENT_VERSION()`) of the database, one gauge for all streams. A change to a tracked table moves it, in the transaction of the change. A `TRUNCATE` does not move it.
|
|
155
|
+
- **The last rowversion** (`@@DBTS`) of the database, one gauge for all streams. An insert or an update of a row with a `rowversion` column moves it.
|
|
156
|
+
- **The statistics** of the table (`sys.dm_db_stats_properties`): their count, the rows changed in their columns, and the time of their last refresh. An insert, a delete, a `TRUNCATE` or an update of a column with statistics moves it. A restart keeps it. A refresh of statistics sets the count to 0 and changes the time. `db_datareader` can read it.
|
|
157
|
+
- **The row count** of the table (`sys.partitions`). An insert, a delete or a `TRUNCATE` moves it, also when SQL Server has not built the statistics of the table. An update does not move it. `db_datareader` can read it. A Change Tracking stream watches it for a `TRUNCATE`. A `TRUNCATE` of an empty table moves nothing, and it removes nothing.
|
|
158
|
+
- **The last write** of the table (`sys.dm_db_index_usage_stats`): the writes and the time of the last write. Each insert, update or delete moves it, also of a column with no statistics. A `TRUNCATE` does not move it. Discovery reads it one time. When the login cannot read it, the streams do not use it, and their description names the grant.
|
|
159
|
+
- **The hour**: a period of one hour, the number of whole hours since the watch started. Each hour, it wakes the streams again, and the read finds a change that the other gauges did not see.
|
|
160
|
+
|
|
161
|
+
The statistics, the row count and the last write are collectors: one key for each table, by its quoted name. A table that a collector does not give, for example a table dropped after discovery, changed, so its stream wakes and its read fails.
|
|
162
|
+
|
|
163
|
+
The queries of one check have 10 seconds. When a gauge fails, for example because its query took longer or a restart of the SQL Server engine dropped the connection, the streams of the gauge fail, as a `system` failure, and the next check reads the gauge again. The other streams keep loading. A server that cannot be reached at the first check leaves the session closed, and the next check opens it again. A login that the server refuses fails every stream as `config`, and the pipeline reads the gauges no more in this watch.
|
|
164
|
+
|
|
165
|
+
The database is current whenever the source reads it, so the source gives no `currentAsOf`.
|
|
166
|
+
|
|
167
|
+
## Limitations
|
|
168
|
+
|
|
169
|
+
- Rowversion tables do not see deletions. A deleted row stays.
|
|
170
|
+
- Without the last write, an update of a column with no statistics loads only at the next hour.
|
|
171
|
+
- SQL Server builds the statistics of a table when a query first needs them. Until then, they count nothing. In a table that no query has read by a column since it was created empty, an insert or a delete moves the row count, and an update waits for the last write or the hour. A table with no key and no index has no statistics at all, so the same is true for it.
|
|
172
|
+
- The source does not read CDC (backlog #2315).
|
|
173
|
+
- The source discovers tables only, not views (backlog #3253).
|
|
174
|
+
- A Change Tracking stream whose position is at the version where its generation began loads every row again at each read, until a tracked change in the database moves the version. Tracking turned on after the last tracked change, and a truncate with no tracked change after it, give such a position. The count of the statistics moves on a truncate, but each tracked change and each refresh of statistics also move it. Thus it cannot show that the table was truncated.
|
|
175
|
+
- A single value longer than the longest JavaScript string (about 512 million characters) fails its stream.
|
|
176
|
+
|
|
177
|
+
## Live checks
|
|
178
|
+
|
|
179
|
+
Run on 2026-10-06 against SQL Server 2022 CU27 (16.0.4295.3) Developer, in the compose `sqlserver` service on linux/amd64, emulated on Apple silicon, through mssql 12.7.4 and tedious 20.3.3:
|
|
180
|
+
|
|
181
|
+
- Style 126 leaves out a zero fraction (`2025-01-02T03:04:05`) and keeps every digit of any other fraction (`.1200000`). `time(p)` keeps exactly `p` digits. Style 2 spells money with four digits.
|
|
182
|
+
- `CHANGE_TRACKING_MIN_VALID_VERSION` gives 0, not NULL, to a login without `VIEW CHANGE TRACKING`. The `CHANGETABLE` read of that login fails with error 229. `sys.change_tracking_tables` and `MS_Description` stay visible to `db_datareader`.
|
|
183
|
+
- `TRUNCATE` sets the table's minimum valid version and `begin_version` to the current version. Enabling tracking again makes a new `create_date` for the change table in `sys.internal_tables`, which `VIEW CHANGE TRACKING` makes visible.
|
|
184
|
+
- A column-level `DENY` makes `HAS_PERMS_BY_NAME` report the full table as not readable. Thus the source checks readability for each column. A column of an alias type that the login cannot see disappears from an inner join with `sys.types`.
|
|
185
|
+
- A key bound as an `nvarchar` parameter and compared with a `varchar` column under `SQL_Latin1_General_CP1_CI_AS` skips rows (`co-op` before `Coat`): the two sort hyphens differently.
|
|
186
|
+
- `varbinary` keys ignore zero bytes at the end (`0x00`, `0x0000` and `0x` are one key).
|
|
187
|
+
- tedious 20.0.0 refused to connect to an IP address over TLS. 20.3.3 connects.
|
|
188
|
+
- `sql_variant` text under style 126 keeps times exact. Floating point needs style 3 for 17 digits.
|
|
189
|
+
- An end-to-end check of the source and the pipeline loaded a seeded database, read by a `db_datareader` login. On a second run, it wrote nothing to the Change Tracking and rowversion tables. On a third run, it applied an update, a deletion, a truncate and an added column.
|
|
190
|
+
|
|
191
|
+
Run on 2026-11-04 against three Azure SQL databases (General Purpose, Gen5), read only:
|
|
192
|
+
|
|
193
|
+
- `sys.dm_db_index_usage_stats` gave the time of the last write to a table to the second of its `updatedAt` column. It also gave a write on 2026-10-29 to a table whose `DateUpdated` column the write did not change. It was empty for writes before the last restart of the engine, and the three engines restarted within 9 days.
|
|
194
|
+
- `sys.dm_db_log_stats` moved its last log position in 15 seconds with no write by a user, so it is no signal of a table.
|
|
195
|
+
|
|
196
|
+
Run on 2026-11-06 against SQL Server 2022 in a container, through a `db_datareader` login and a login with more grants:
|
|
197
|
+
|
|
198
|
+
- A restart cleared the last writes and the row counts of `sys.dm_db_index_operational_stats`, and kept the count of `sys.dm_db_stats_properties`.
|
|
199
|
+
- A `TRUNCATE` moved neither the last write nor the row counts. It moved the count of the statistics.
|
|
200
|
+
- An update of 100 rows in a column with no statistics did not move the count of the statistics. `UPDATE STATISTICS` set the count to 0 and changed the time of the refresh.
|
|
201
|
+
- An `UPDATE` of no row and an `UPDATE` that was rolled back moved the last write.
|
|
202
|
+
- `db_datareader` could read the count of the statistics and the row counts of `sys.partitions`, and got error 297 for the last writes and the row counts of `sys.dm_db_index_operational_stats`.
|
|
203
|
+
- Statistics that an index created on an empty table made had no count and no time of refresh until `UPDATE STATISTICS` built them. Rows inserted before then moved only the row count.
|
|
204
|
+
- `CHECKSUM_AGG(BINARY_CHECKSUM(*))` read every page of the table. It did not change for a value changed from `' !'` to `''Q'`, for two rows that swapped a value, or for two equal rows added to a table with no key. A SHA-256 hash of each row found all three.
|
|
205
|
+
|
|
206
|
+
Run on 2026-11-07 against SQL Server 2022 in a container:
|
|
207
|
+
|
|
208
|
+
- A `TRUNCATE` of a tracked table with rows did not move `CHANGE_TRACKING_CURRENT_VERSION()`. It moved the row count to 0, and `begin_version` and the minimum valid version to the current version. It did not change the `partition_id` of the table.
|
|
209
|
+
- A `SELECT` with no `WHERE` did not build the statistics of a table created empty. A `SELECT` with a `WHERE` on the key built them: the count went from none to 0, with a time of refresh. A `TRUNCATE` did not move statistics that were not built.
|
|
210
|
+
- A disabled login and a login to a database that is `OFFLINE` both failed with `ELOGIN` that tedious did not mark as transient. The error log had one `Login failed` line for each attempt.
|
|
211
|
+
|
|
212
|
+
## Decisions
|
|
213
|
+
|
|
214
|
+
- [ADR 0001: A Change Tracking position carries the tracking generation](./docs/adr/0001-a-change-tracking-position-carries-the-tracking-generation.md)
|
|
215
|
+
- [ADR 0002: Each column is read as the text that SQL Server spells](./docs/adr/0002-each-column-is-read-as-the-text-that-sql-server-spells.md)
|
|
216
|
+
- [ADR 0003: An added field takes the next free name](./docs/adr/0003-an-added-field-takes-the-next-free-name.md)
|
|
217
|
+
- [ADR 0004: A copy ID carries the source identity](./docs/adr/0004-a-copy-id-carries-the-source-identity.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CopyConfiguration, ExtractionCoverage, SourceMessage } from '@eltjs/elt';
|
|
2
|
+
import { type SqlServerSession, type SqlServerTable } from '@eltjs/sdk-microsoft-sql-server';
|
|
3
|
+
import { SqlServerStream } from './sql-server-stream.ts';
|
|
4
|
+
import type { WatchGauges } from './watch-gauges.ts';
|
|
5
|
+
export declare class ChangeTrackingStream extends SqlServerStream {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(table: SqlServerTable);
|
|
8
|
+
gauges(watch: WatchGauges): import("@eltjs/elt").Gauge<import("@zukhruf/async").Value>[];
|
|
9
|
+
coverage(): ExtractionCoverage;
|
|
10
|
+
extract(configuration: CopyConfiguration, state: unknown, session: SqlServerSession): AsyncGenerator<SourceMessage>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ChangeHistoryExpiredError, } from '@eltjs/sdk-microsoft-sql-server';
|
|
2
|
+
import { SqlServerStream, pageSize } from './sql-server-stream.js';
|
|
3
|
+
function position(value) {
|
|
4
|
+
return typeof value === 'object' &&
|
|
5
|
+
value !== null &&
|
|
6
|
+
'version' in value &&
|
|
7
|
+
typeof value.version === 'string' &&
|
|
8
|
+
'generation' in value &&
|
|
9
|
+
typeof value.generation === 'string'
|
|
10
|
+
? { version: value.version, generation: value.generation }
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function parse(state) {
|
|
14
|
+
if (state === null)
|
|
15
|
+
return null;
|
|
16
|
+
const fields = typeof state === 'object' &&
|
|
17
|
+
'fields' in state &&
|
|
18
|
+
Array.isArray(state.fields) &&
|
|
19
|
+
state.fields.every((field) => typeof field === 'string')
|
|
20
|
+
? state.fields
|
|
21
|
+
: undefined;
|
|
22
|
+
if (fields === undefined)
|
|
23
|
+
throw new TypeError(`Unknown Change Tracking state ${JSON.stringify(state)}`);
|
|
24
|
+
if (typeof state === 'object' && 'from' in state) {
|
|
25
|
+
const from = position(state.from);
|
|
26
|
+
if (from !== undefined)
|
|
27
|
+
return { from, fields };
|
|
28
|
+
}
|
|
29
|
+
if (typeof state === 'object' &&
|
|
30
|
+
'initial' in state &&
|
|
31
|
+
typeof state.initial === 'object' &&
|
|
32
|
+
state.initial !== null &&
|
|
33
|
+
'from' in state.initial &&
|
|
34
|
+
'after' in state.initial &&
|
|
35
|
+
Array.isArray(state.initial.after) &&
|
|
36
|
+
state.initial.after.every((part) => typeof part === 'string')) {
|
|
37
|
+
const from = position(state.initial.from);
|
|
38
|
+
if (from !== undefined)
|
|
39
|
+
return { initial: { from, after: state.initial.after }, fields };
|
|
40
|
+
}
|
|
41
|
+
throw new TypeError(`Unknown Change Tracking state ${JSON.stringify(state)}`);
|
|
42
|
+
}
|
|
43
|
+
// A table SQL Server tracks changes of, which the login may read: the first
|
|
44
|
+
// read loads every row, page by page, from the position current before it
|
|
45
|
+
// began; later reads apply what changed since, deletions included. When the
|
|
46
|
+
// history no longer reaches back to the saved position (cleanup, a truncate,
|
|
47
|
+
// tracking turned off and on), the stream starts over with a RESET and loads
|
|
48
|
+
// every row again, as it does for a column the table gained: Change Tracking
|
|
49
|
+
// reports no change for the rows it was added to.
|
|
50
|
+
export class ChangeTrackingStream extends SqlServerStream {
|
|
51
|
+
constructor(table) {
|
|
52
|
+
super(table, {
|
|
53
|
+
supportedSyncModes: ['full_refresh', 'incremental'],
|
|
54
|
+
sourceDefinedCursor: true,
|
|
55
|
+
emitsDeletes: true,
|
|
56
|
+
strategy: 'Read through SQL Server Change Tracking: inserts, updates and deletions apply.',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// A TRUNCATE moves no Change Tracking version, but it moves the row count
|
|
60
|
+
// of a table that held rows.
|
|
61
|
+
gauges(watch) {
|
|
62
|
+
return [watch.changeTracking, watch.rowCounts.gauge(this.table.quoted)];
|
|
63
|
+
}
|
|
64
|
+
coverage() {
|
|
65
|
+
return {
|
|
66
|
+
description: `Every row of ${this.table.quoted} the login can read, then what Change Tracking reports changed`,
|
|
67
|
+
selection: { table: this.table.quoted },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async *extract(configuration, state, session) {
|
|
71
|
+
if (configuration.syncMode === 'full_refresh') {
|
|
72
|
+
yield* this.everyRow(session);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const saved = parse(state);
|
|
76
|
+
if (saved === null) {
|
|
77
|
+
yield* this.#loadEveryRow(session);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (this.gained(saved.fields)) {
|
|
81
|
+
yield { type: 'RESET', stream: this.stream.name };
|
|
82
|
+
yield* this.#loadEveryRow(session);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if ('initial' in saved) {
|
|
86
|
+
yield* this.#load(session, saved.initial.from, saved.initial.after);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
yield* this.#changes(session, saved.from);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!(error instanceof ChangeHistoryExpiredError))
|
|
94
|
+
throw error;
|
|
95
|
+
// Whatever the expired read already yielded, the reset drops.
|
|
96
|
+
yield { type: 'RESET', stream: this.stream.name };
|
|
97
|
+
yield* this.#loadEveryRow(session);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Every row, from where the table's change history stands now.
|
|
101
|
+
async *#loadEveryRow(session) {
|
|
102
|
+
yield* this.#load(session, await session.changeTrackingPosition(this.table), null);
|
|
103
|
+
}
|
|
104
|
+
// Every row, from a position read before the first page, so a change made
|
|
105
|
+
// during the load is read again by the next delta.
|
|
106
|
+
async *#load(session, from, after) {
|
|
107
|
+
const { name } = this.stream;
|
|
108
|
+
const fields = this.fieldNames;
|
|
109
|
+
if (!this.table.pageable) {
|
|
110
|
+
yield* this.everyRow(session);
|
|
111
|
+
yield { type: 'STATE', stream: name, state: { from, fields } };
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let last = after;
|
|
115
|
+
for (;;) {
|
|
116
|
+
let count = 0;
|
|
117
|
+
for await (const row of session.page(this.table, last, pageSize)) {
|
|
118
|
+
yield this.message(row);
|
|
119
|
+
last = row.position;
|
|
120
|
+
count += 1;
|
|
121
|
+
}
|
|
122
|
+
if (count < pageSize)
|
|
123
|
+
break;
|
|
124
|
+
yield {
|
|
125
|
+
type: 'STATE',
|
|
126
|
+
stream: name,
|
|
127
|
+
state: { initial: { from, after: last }, fields },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
yield { type: 'STATE', stream: name, state: { from, fields } };
|
|
131
|
+
}
|
|
132
|
+
async *#changes(session, since) {
|
|
133
|
+
const { name } = this.stream;
|
|
134
|
+
let reached;
|
|
135
|
+
{
|
|
136
|
+
await using set = await session.changes(this.table, since);
|
|
137
|
+
for await (const change of set)
|
|
138
|
+
yield change.type === 'upsert'
|
|
139
|
+
? this.message(change.row)
|
|
140
|
+
: { type: 'DELETE', stream: name, key: this.fields.key(change.key) };
|
|
141
|
+
reached = set.position;
|
|
142
|
+
}
|
|
143
|
+
yield {
|
|
144
|
+
type: 'STATE',
|
|
145
|
+
stream: name,
|
|
146
|
+
state: { from: reached, fields: this.fieldNames },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CopyConfiguration, ExtractionCoverage, SourceMessage } from '@eltjs/elt';
|
|
2
|
+
import type { SqlServerSession, SqlServerTable } from '@eltjs/sdk-microsoft-sql-server';
|
|
3
|
+
import { SqlServerStream } from './sql-server-stream.ts';
|
|
4
|
+
import type { WatchGauges } from './watch-gauges.ts';
|
|
5
|
+
import type { WholeTableGauges } from './whole-table-gauges.ts';
|
|
6
|
+
export declare class FullRefreshStream extends SqlServerStream {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(table: SqlServerTable, whole: WholeTableGauges);
|
|
9
|
+
gauges(watch: WatchGauges): readonly import("@eltjs/elt").Gauge<import("@zukhruf/async").Value>[];
|
|
10
|
+
coverage(): ExtractionCoverage;
|
|
11
|
+
extract(_configuration: CopyConfiguration, _state: unknown, session: SqlServerSession): AsyncIterable<SourceMessage>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SqlServerStream } from './sql-server-stream.js';
|
|
2
|
+
// A table with no key the login can read in full, so no change can apply by
|
|
3
|
+
// key: every read takes every row, and a copy replaces what it loaded before.
|
|
4
|
+
export class FullRefreshStream extends SqlServerStream {
|
|
5
|
+
#whole;
|
|
6
|
+
constructor(table, whole) {
|
|
7
|
+
super(table, {
|
|
8
|
+
supportedSyncModes: ['full_refresh'],
|
|
9
|
+
strategy: `Read in full on every sync.${whole.hint}`,
|
|
10
|
+
});
|
|
11
|
+
this.#whole = whole;
|
|
12
|
+
}
|
|
13
|
+
gauges(watch) {
|
|
14
|
+
return this.#whole.gauges(this.table, watch);
|
|
15
|
+
}
|
|
16
|
+
coverage() {
|
|
17
|
+
return {
|
|
18
|
+
description: `Every row of ${this.table.quoted} the login can read`,
|
|
19
|
+
selection: { table: this.table.quoted },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
extract(_configuration, _state, session) {
|
|
23
|
+
return this.everyRow(session);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CopyConfiguration, ExtractionCoverage, SourceMessage } from '@eltjs/elt';
|
|
2
|
+
import type { SqlServerSession, SqlServerTable } from '@eltjs/sdk-microsoft-sql-server';
|
|
3
|
+
import { SqlServerStream } from './sql-server-stream.ts';
|
|
4
|
+
import type { WatchGauges } from './watch-gauges.ts';
|
|
5
|
+
export declare class RowversionStream extends SqlServerStream {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(table: SqlServerTable);
|
|
8
|
+
gauges(watch: WatchGauges): import("@eltjs/elt").Gauge<import("@zukhruf/async").Value>[];
|
|
9
|
+
coverage(): ExtractionCoverage;
|
|
10
|
+
extract(configuration: CopyConfiguration, state: unknown, session: SqlServerSession): AsyncGenerator<SourceMessage>;
|
|
11
|
+
}
|