@eltjs/elt 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 +1153 -2
- package/dist/core/catalog.d.ts +7 -0
- package/dist/core/catalog.js +23 -0
- package/dist/core/change-signal.d.ts +9 -0
- package/dist/core/change-signal.js +4 -0
- package/dist/core/clock.d.ts +6 -0
- package/dist/core/clock.js +1 -0
- package/dist/core/connection.d.ts +20 -0
- package/dist/core/connection.js +31 -0
- package/dist/core/copy-configuration.d.ts +24 -0
- package/dist/core/copy-configuration.js +120 -0
- package/dist/core/copy.d.ts +27 -0
- package/dist/core/copy.js +56 -0
- package/dist/core/deduplication.d.ts +14 -0
- package/dist/core/deduplication.js +96 -0
- package/dist/core/derived-dataset.d.ts +19 -0
- package/dist/core/derived-dataset.js +10 -0
- package/dist/core/derived-refreshes.d.ts +15 -0
- package/dist/core/derived-refreshes.js +120 -0
- package/dist/core/destination.d.ts +22 -0
- package/dist/core/destination.js +17 -0
- package/dist/core/document-parser.d.ts +6 -0
- package/dist/core/document-parser.js +15 -0
- package/dist/core/document-text.d.ts +28 -0
- package/dist/core/document-text.js +72 -0
- package/dist/core/file-content.d.ts +5 -0
- package/dist/core/file-content.js +24 -0
- package/dist/core/file-extension.d.ts +1 -0
- package/dist/core/file-extension.js +9 -0
- package/dist/core/file-read.d.ts +20 -0
- package/dist/core/file-read.js +79 -0
- package/dist/core/file-storage.d.ts +8 -0
- package/dist/core/file-storage.js +4 -0
- package/dist/core/file-transfer.d.ts +12 -0
- package/dist/core/file-transfer.js +65 -0
- package/dist/core/formats/base64-format.d.ts +6 -0
- package/dist/core/formats/base64-format.js +13 -0
- package/dist/core/formats/calendar-date-format.d.ts +6 -0
- package/dist/core/formats/calendar-date-format.js +13 -0
- package/dist/core/formats/decimal-format.d.ts +9 -0
- package/dist/core/formats/decimal-format.js +41 -0
- package/dist/core/formats/declared-format.d.ts +12 -0
- package/dist/core/formats/declared-format.js +48 -0
- package/dist/core/formats/field-format.d.ts +6 -0
- package/dist/core/formats/field-format.js +10 -0
- package/dist/core/formats/int64-format.d.ts +6 -0
- package/dist/core/formats/int64-format.js +16 -0
- package/dist/core/formats/local-time-format.d.ts +6 -0
- package/dist/core/formats/local-time-format.js +10 -0
- package/dist/core/formats/local-timestamp-format.d.ts +6 -0
- package/dist/core/formats/local-timestamp-format.js +10 -0
- package/dist/core/formats/temporal-format.d.ts +7 -0
- package/dist/core/formats/temporal-format.js +23 -0
- package/dist/core/formats/timestamp-format.d.ts +7 -0
- package/dist/core/formats/timestamp-format.js +14 -0
- package/dist/core/identifiers.d.ts +10 -0
- package/dist/core/identifiers.js +51 -0
- package/dist/core/interleave.d.ts +1 -0
- package/dist/core/interleave.js +38 -0
- package/dist/core/partition.d.ts +16 -0
- package/dist/core/partition.js +60 -0
- package/dist/core/pass-channels.d.ts +46 -0
- package/dist/core/pass-channels.js +65 -0
- package/dist/core/pipeline.d.ts +37 -0
- package/dist/core/pipeline.js +546 -0
- package/dist/core/reader-relation.d.ts +6 -0
- package/dist/core/reader-relation.js +11 -0
- package/dist/core/record-validation.d.ts +58 -0
- package/dist/core/record-validation.js +128 -0
- package/dist/core/reload-mode.d.ts +9 -0
- package/dist/core/reload-mode.js +17 -0
- package/dist/core/replication.d.ts +8 -0
- package/dist/core/replication.js +478 -0
- package/dist/core/rule.d.ts +29 -0
- package/dist/core/rule.js +6 -0
- package/dist/core/signal-watch.d.ts +11 -0
- package/dist/core/signal-watch.js +110 -0
- package/dist/core/snapshot.d.ts +17 -0
- package/dist/core/snapshot.js +111 -0
- package/dist/core/source.d.ts +79 -0
- package/dist/core/source.js +227 -0
- package/dist/core/stream-change.d.ts +5 -0
- package/dist/core/stream-change.js +31 -0
- package/dist/core/stream.d.ts +26 -0
- package/dist/core/stream.js +86 -0
- package/dist/core/target-description.d.ts +15 -0
- package/dist/core/target-description.js +73 -0
- package/dist/core/target.d.ts +5 -0
- package/dist/core/target.js +14 -0
- package/dist/core/upkeep.d.ts +4 -0
- package/dist/core/upkeep.js +1 -0
- package/dist/core/writer.d.ts +41 -0
- package/dist/core/writer.js +17 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +36 -0
- package/dist/state/checkpoint-store.d.ts +29 -0
- package/dist/state/checkpoint-store.js +81 -0
- package/dist/state/sync-history-relations.d.ts +72 -0
- package/dist/state/sync-history-relations.js +74 -0
- package/dist/state/sync-history.d.ts +37 -0
- package/dist/state/sync-history.js +80 -0
- package/dist/storage/local-files.d.ts +15 -0
- package/dist/storage/local-files.js +121 -0
- package/package.json +48 -4
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Deduplication } from './deduplication.js';
|
|
2
|
+
import { FileReference } from './file-read.js';
|
|
3
|
+
// A source's immutable description of a resource, without extraction behavior.
|
|
4
|
+
export class Stream {
|
|
5
|
+
name;
|
|
6
|
+
jsonSchema;
|
|
7
|
+
primaryKey;
|
|
8
|
+
supportedSyncModes;
|
|
9
|
+
supportsFileTransfer;
|
|
10
|
+
// Incremental progress is opaque source state, so copies select no cursorField.
|
|
11
|
+
sourceDefinedCursor;
|
|
12
|
+
// Incremental reads may emit DELETE messages keyed by primaryKey.
|
|
13
|
+
emitsDeletes;
|
|
14
|
+
// The source reads the stream once per value of these primaryKey fields and
|
|
15
|
+
// keeps each partition's state apart; every record carries its partition.
|
|
16
|
+
partitionKey;
|
|
17
|
+
// The upstream drops records once this timestamp field passes its retention,
|
|
18
|
+
// without a deletion: a record that vanished before the read's horizon
|
|
19
|
+
// expired, and its row stays loaded.
|
|
20
|
+
expiresBy;
|
|
21
|
+
constructor({ name, jsonSchema, primaryKey = [], supportedSyncModes, supportsFileTransfer, sourceDefinedCursor, emitsDeletes, partitionKey, expiresBy, }) {
|
|
22
|
+
if (!name || name.includes('\0'))
|
|
23
|
+
throw new TypeError('Invalid stream name');
|
|
24
|
+
if (!Array.isArray(supportedSyncModes) ||
|
|
25
|
+
supportedSyncModes.length === 0 ||
|
|
26
|
+
!supportedSyncModes.every((mode) => mode === 'full_refresh' || mode === 'incremental') ||
|
|
27
|
+
new Set(supportedSyncModes).size !== supportedSyncModes.length)
|
|
28
|
+
throw new TypeError('A stream requires distinct supported sync modes');
|
|
29
|
+
this.name = name;
|
|
30
|
+
if (supportsFileTransfer !== undefined && supportsFileTransfer !== true)
|
|
31
|
+
throw new TypeError('supportsFileTransfer must be true when declared');
|
|
32
|
+
this.supportsFileTransfer = supportsFileTransfer;
|
|
33
|
+
if (sourceDefinedCursor !== undefined && sourceDefinedCursor !== true)
|
|
34
|
+
throw new TypeError('sourceDefinedCursor must be true when declared');
|
|
35
|
+
if (emitsDeletes !== undefined && emitsDeletes !== true)
|
|
36
|
+
throw new TypeError('emitsDeletes must be true when declared');
|
|
37
|
+
if ((sourceDefinedCursor || emitsDeletes) &&
|
|
38
|
+
!supportedSyncModes.includes('incremental'))
|
|
39
|
+
throw new TypeError('A source-defined cursor or deletions require incremental support');
|
|
40
|
+
if (emitsDeletes && primaryKey.length === 0)
|
|
41
|
+
throw new TypeError('A stream that emits deletions requires a primaryKey');
|
|
42
|
+
this.sourceDefinedCursor = sourceDefinedCursor;
|
|
43
|
+
this.emitsDeletes = emitsDeletes;
|
|
44
|
+
this.jsonSchema = structuredClone(jsonSchema);
|
|
45
|
+
// Snapshot nested metadata too, so changes in a source cannot rewrite a pipeline.
|
|
46
|
+
const seen = new WeakSet();
|
|
47
|
+
const freeze = (value) => {
|
|
48
|
+
if (value === null || typeof value !== 'object' || seen.has(value))
|
|
49
|
+
return;
|
|
50
|
+
seen.add(value);
|
|
51
|
+
for (const child of Object.values(value))
|
|
52
|
+
freeze(child);
|
|
53
|
+
Object.freeze(value);
|
|
54
|
+
};
|
|
55
|
+
freeze(this.jsonSchema);
|
|
56
|
+
this.primaryKey = Object.freeze([...primaryKey]);
|
|
57
|
+
this.supportedSyncModes = Object.freeze([...supportedSyncModes]);
|
|
58
|
+
if (partitionKey !== undefined) {
|
|
59
|
+
if (!Array.isArray(partitionKey) ||
|
|
60
|
+
partitionKey.length === 0 ||
|
|
61
|
+
new Set(partitionKey).size !== partitionKey.length ||
|
|
62
|
+
!partitionKey.every((field) => primaryKey.includes(field)))
|
|
63
|
+
throw new TypeError('partitionKey fields must be distinct members of primaryKey');
|
|
64
|
+
this.partitionKey = Object.freeze([...partitionKey]);
|
|
65
|
+
new Deduplication(this, this.partitionKey);
|
|
66
|
+
}
|
|
67
|
+
if (expiresBy !== undefined) {
|
|
68
|
+
const field = jsonSchema.properties?.[expiresBy];
|
|
69
|
+
if (!emitsDeletes ||
|
|
70
|
+
field === undefined ||
|
|
71
|
+
field.type !== 'string' ||
|
|
72
|
+
field.format !== 'date-time')
|
|
73
|
+
throw new TypeError('expiresBy must name a non-null date-time property of a stream that emits deletions');
|
|
74
|
+
// Snapshots compare expiry with a millisecond horizon as text.
|
|
75
|
+
if (field.precision !== undefined && field.precision !== 3)
|
|
76
|
+
throw new TypeError(`expiresBy ${expiresBy} must keep milliseconds; it declares precision ${field.precision}`);
|
|
77
|
+
this.expiresBy = expiresBy;
|
|
78
|
+
}
|
|
79
|
+
Object.freeze(this);
|
|
80
|
+
}
|
|
81
|
+
get file() {
|
|
82
|
+
if (this.supportsFileTransfer !== true)
|
|
83
|
+
throw new TypeError(`Stream ${this.name} does not support file extraction`);
|
|
84
|
+
return new FileReference(this);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CopyConfiguration } from './copy-configuration.ts';
|
|
2
|
+
import type { FileRead } from './file-read.ts';
|
|
3
|
+
export type DescribedColumn = {
|
|
4
|
+
readonly field: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly fileRead?: FileRead;
|
|
7
|
+
readonly storesFile: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type TargetDescription = {
|
|
10
|
+
readonly meaning: string | null;
|
|
11
|
+
readonly table: string;
|
|
12
|
+
readonly columns: Readonly<Record<string, string | null>>;
|
|
13
|
+
readonly missing: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
export declare function describeTarget<Column extends DescribedColumn>(configuration: CopyConfiguration, columns: readonly Column[], storedFile: (column: Column) => string): TargetDescription;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// JSON Schema annotations are optional; absent ones describe nothing.
|
|
2
|
+
function annotation(value) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return null;
|
|
5
|
+
if (typeof value !== 'string' ||
|
|
6
|
+
value.includes('\0') ||
|
|
7
|
+
!value.isWellFormed())
|
|
8
|
+
throw new TypeError('JSON Schema description must be well-formed text without NUL');
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
const loading = {
|
|
12
|
+
append: 'Every accepted observation is appended; source keys may repeat.',
|
|
13
|
+
overwrite: 'Each full refresh replaces the table with its accepted records.',
|
|
14
|
+
append_dedup: 'Accepted observations reconcile rows by the copy key.',
|
|
15
|
+
overwrite_dedup: 'Each full refresh replaces the table with deduplicated records.',
|
|
16
|
+
};
|
|
17
|
+
// What every destination tells readers about a copy's target: the stream's
|
|
18
|
+
// meaning, how it is loaded, and each column from the stream's schema. Only
|
|
19
|
+
// where original file bytes are kept differs by destination.
|
|
20
|
+
export function describeTarget(configuration, columns, storedFile) {
|
|
21
|
+
const { stream } = configuration;
|
|
22
|
+
const meaning = annotation(stream.jsonSchema.description);
|
|
23
|
+
const lines = [`Source stream: ${stream.name}.`];
|
|
24
|
+
if (meaning !== null)
|
|
25
|
+
lines.push(`Source record meaning: ${meaning}`);
|
|
26
|
+
lines.push(`Extraction: ${configuration.syncMode}. Loading: ${configuration.destinationSyncMode}. ${loading[configuration.destinationSyncMode]}`);
|
|
27
|
+
const named = (field) => columns.find((column) => column.field === field)?.name ?? field;
|
|
28
|
+
if (configuration.dedupPolicy !== undefined) {
|
|
29
|
+
const deduplication = configuration.deduplication();
|
|
30
|
+
const { primaryKey, cursorField } = deduplication;
|
|
31
|
+
lines.push(`Copy key: ${primaryKey.map(named).join(', ')}.`);
|
|
32
|
+
lines.push(configuration.dedupPolicy === 'replace' || cursorField === undefined
|
|
33
|
+
? 'For a repeated key, the newest extracted record wins.'
|
|
34
|
+
: `For a repeated key, the greatest ${named(cursorField)} wins; equal cursors retain the first accepted record. Text cursors compare ${textOrdering(deduplication.format(cursorField))}.`);
|
|
35
|
+
}
|
|
36
|
+
const { properties } = stream.jsonSchema;
|
|
37
|
+
const missing = meaning === null ? ['the stream'] : [];
|
|
38
|
+
const describe = (column) => {
|
|
39
|
+
if (column.storesFile)
|
|
40
|
+
return storedFile(column);
|
|
41
|
+
if (column.fileRead?.parser !== undefined)
|
|
42
|
+
return `Text extracted from the source file by parser ${column.fileRead.parser.identity}. NULL when the source file is unavailable or the parser returns no text.`;
|
|
43
|
+
if (column.fileRead?.file.storage !== undefined)
|
|
44
|
+
return `${column.fileRead.file.storage.reference} NULL when the source file is unavailable.`;
|
|
45
|
+
const description = annotation(properties?.[column.field]?.description);
|
|
46
|
+
if (description === null)
|
|
47
|
+
missing.push(column.field);
|
|
48
|
+
return description;
|
|
49
|
+
};
|
|
50
|
+
const described = Object.fromEntries(columns.map((column) => {
|
|
51
|
+
const description = describe(column);
|
|
52
|
+
if (column.name === column.field)
|
|
53
|
+
return [column.name, description];
|
|
54
|
+
const source = `Source field: ${JSON.stringify(column.field)}.`;
|
|
55
|
+
return [
|
|
56
|
+
column.name,
|
|
57
|
+
description === null ? source : `${description} ${source}`,
|
|
58
|
+
];
|
|
59
|
+
}));
|
|
60
|
+
described.loaded_at =
|
|
61
|
+
'Start time of the load that last wrote this row, not the source modification time or the most recent successful sync.';
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
meaning,
|
|
64
|
+
table: lines.join('\n'),
|
|
65
|
+
columns: Object.freeze(described),
|
|
66
|
+
missing: Object.freeze(missing),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// How a text cursor compares: as its declared format orders it, or by byte
|
|
70
|
+
// order, as a plain string compares, when it declares none.
|
|
71
|
+
function textOrdering(format) {
|
|
72
|
+
return format === null ? 'by byte order' : format.ordering;
|
|
73
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FileRead } from './file-read.js';
|
|
2
|
+
// A destination declaration supplies its source requirements without doing I/O.
|
|
3
|
+
export class Target {
|
|
4
|
+
fileReads;
|
|
5
|
+
constructor(fileReads = []) {
|
|
6
|
+
if (!Array.isArray(fileReads) ||
|
|
7
|
+
!fileReads.every((read) => read instanceof FileRead))
|
|
8
|
+
throw new TypeError('Targets require FileRead declarations');
|
|
9
|
+
const names = fileReads.map((read) => read.name.toLowerCase());
|
|
10
|
+
if (new Set(names).size !== names.length)
|
|
11
|
+
throw new TypeError('Duplicate file field names');
|
|
12
|
+
this.fileReads = Object.freeze([...fileReads]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Partition } from './partition.ts';
|
|
2
|
+
import type { FailureType, KeyValue } from './source.ts';
|
|
3
|
+
import type { Stream } from './stream.ts';
|
|
4
|
+
export type WriteCount = {
|
|
5
|
+
readonly count: number;
|
|
6
|
+
readonly deleted: number;
|
|
7
|
+
};
|
|
8
|
+
export declare class TargetOwnedError extends TypeError {
|
|
9
|
+
name: string;
|
|
10
|
+
constructor(target: string, owner: string, writer: string);
|
|
11
|
+
}
|
|
12
|
+
export type LoadFailure = {
|
|
13
|
+
readonly partition: Partition | null;
|
|
14
|
+
readonly error: unknown;
|
|
15
|
+
readonly failureType: FailureType;
|
|
16
|
+
};
|
|
17
|
+
export type WriteOperation = {
|
|
18
|
+
readonly type: 'RECORD';
|
|
19
|
+
readonly data: unknown;
|
|
20
|
+
} | {
|
|
21
|
+
readonly type: 'DELETE';
|
|
22
|
+
readonly key: Readonly<Record<string, KeyValue>>;
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: 'RESET';
|
|
25
|
+
readonly partition: Partition | null;
|
|
26
|
+
};
|
|
27
|
+
export type FieldValues = (field: string) => AsyncIterable<unknown>;
|
|
28
|
+
export type Stage = AsyncDisposable & {
|
|
29
|
+
readonly fresh: boolean;
|
|
30
|
+
readonly reloading: boolean;
|
|
31
|
+
complete(): Promise<void>;
|
|
32
|
+
values: FieldValues;
|
|
33
|
+
apply(operation: WriteOperation): Promise<void>;
|
|
34
|
+
commit(): Promise<void>;
|
|
35
|
+
discard(): Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
export declare abstract class Writer {
|
|
38
|
+
readonly stream: Stream;
|
|
39
|
+
constructor(stream: Stream);
|
|
40
|
+
abstract clear(writer: string, committed?: (values: FieldValues) => Promise<void>): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// A target has one writer, as in Airbyte: an overwrite replaces the whole
|
|
2
|
+
// target and a snapshot deletes keys it once saw, so a second writer's rows
|
|
3
|
+
// would be lost. Clearing the target releases it.
|
|
4
|
+
export class TargetOwnedError extends TypeError {
|
|
5
|
+
name = 'TargetOwnedError';
|
|
6
|
+
constructor(target, owner, writer) {
|
|
7
|
+
super(`Target ${target} is written by ${owner}; ${writer} cannot write it. A target has one writer; clear the owning copy, or drop the target, to reassign it.`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// A destination's loading strategy for one stream; its Load prepares the
|
|
11
|
+
// stream's Stage.
|
|
12
|
+
export class Writer {
|
|
13
|
+
stream;
|
|
14
|
+
constructor(stream) {
|
|
15
|
+
this.stream = stream;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export { Catalog } from './core/catalog.ts';
|
|
2
|
+
export type { PipelineClock } from './core/clock.ts';
|
|
3
|
+
export { Connection } from './core/connection.ts';
|
|
4
|
+
export { Copy, type CopyOutcome, type CopyResult } from './core/copy.ts';
|
|
5
|
+
export { CopyConfiguration, type DedupPolicy, } from './core/copy-configuration.ts';
|
|
6
|
+
export type { Deduplication } from './core/deduplication.ts';
|
|
7
|
+
export { Destination, type DestinationSyncMode, type Load, } from './core/destination.ts';
|
|
8
|
+
export { DerivedDataset, type RefreshTrigger } from './core/derived-dataset.ts';
|
|
9
|
+
export { DocumentParser } from './core/document-parser.ts';
|
|
10
|
+
export { DocumentText } from './core/document-text.ts';
|
|
11
|
+
export { type LoadedRow, Rule, type RuleColumn, type RuleResult, } from './core/rule.ts';
|
|
12
|
+
export { CopyProgress, PassFailed, PassFinished, PassStarted, passChannels, } from './core/pass-channels.ts';
|
|
13
|
+
export { FileContent } from './core/file-content.ts';
|
|
14
|
+
export { fileExtension } from './core/file-extension.ts';
|
|
15
|
+
export { FileRead, FileReference } from './core/file-read.ts';
|
|
16
|
+
export { FileStorage } from './core/file-storage.ts';
|
|
17
|
+
export { isCalendarDate } from './core/formats/calendar-date-format.ts';
|
|
18
|
+
export { type DeclaredFormat, declaredFormat, } from './core/formats/declared-format.ts';
|
|
19
|
+
export { Int64Format } from './core/formats/int64-format.ts';
|
|
20
|
+
export { LocalTimestampFormat } from './core/formats/local-timestamp-format.ts';
|
|
21
|
+
export { isTimestamp } from './core/formats/timestamp-format.ts';
|
|
22
|
+
export { Identifiers } from './core/identifiers.ts';
|
|
23
|
+
export type { Partition, PartitionState } from './core/partition.ts';
|
|
24
|
+
export { type Pass, Pipeline, PipelineError, WatchError, } from './core/pipeline.ts';
|
|
25
|
+
export { type ReaderRelation, readerCatalog } from './core/reader-relation.ts';
|
|
26
|
+
export { type ReloadMode, type StoredFit, reloadMode, } from './core/reload-mode.ts';
|
|
27
|
+
export { StreamChangeError } from './core/stream-change.ts';
|
|
28
|
+
export { type FieldSchema, type Properties, type RecordDraft, type SchemaRecord, type StreamSchema, validateRecords, } from './core/record-validation.ts';
|
|
29
|
+
export { diffGroupedSnapshot, diffSnapshot, type KeptGroup, type SnapshotDiffOptions, } from './core/snapshot.ts';
|
|
30
|
+
export { type DeleteMessage, type ExtractionCoverage, type FailureType, type JsonValue, type KeyValue, type ReadMessage, type RecordMessage, type ResetMessage, Source, type SourceMessage, type SourceReadOptions, type StateMessage, StreamStatus, } from './core/source.ts';
|
|
31
|
+
export { type ChangeSignal, type Check, Gauge } from './core/change-signal.ts';
|
|
32
|
+
export { type Upkeep } from './core/upkeep.ts';
|
|
33
|
+
export { Stream, type SyncMode } from './core/stream.ts';
|
|
34
|
+
export { Target } from './core/target.ts';
|
|
35
|
+
export { type DescribedColumn, describeTarget, type TargetDescription, } from './core/target-description.ts';
|
|
36
|
+
export { type FieldValues, type LoadFailure, type Stage, TargetOwnedError, type WriteCount, type WriteOperation, Writer, } from './core/writer.ts';
|
|
37
|
+
export { type CheckpointBinding, type CheckpointRun, type CheckpointSession, CheckpointStore, type StoredCheckpoint, } from './state/checkpoint-store.ts';
|
|
38
|
+
export { copyStatus, type DeclaredCopy, errorMessage, passError, passFailureType, passStatus, type RecordedPass, recordedFailure, type StreamCheck, SyncHistory, type SyncStatus, } from './state/sync-history.ts';
|
|
39
|
+
export { syncHistoryRelations } from './state/sync-history-relations.ts';
|
|
40
|
+
export { LocalFiles } from './storage/local-files.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export { Catalog } from './core/catalog.js';
|
|
2
|
+
export { Connection } from './core/connection.js';
|
|
3
|
+
export { Copy } from './core/copy.js';
|
|
4
|
+
export { CopyConfiguration, } from './core/copy-configuration.js';
|
|
5
|
+
export { Destination, } from './core/destination.js';
|
|
6
|
+
export { DerivedDataset } from './core/derived-dataset.js';
|
|
7
|
+
export { DocumentParser } from './core/document-parser.js';
|
|
8
|
+
export { DocumentText } from './core/document-text.js';
|
|
9
|
+
export { Rule, } from './core/rule.js';
|
|
10
|
+
export { CopyProgress, PassFailed, PassFinished, PassStarted, passChannels, } from './core/pass-channels.js';
|
|
11
|
+
export { FileContent } from './core/file-content.js';
|
|
12
|
+
export { fileExtension } from './core/file-extension.js';
|
|
13
|
+
export { FileRead, FileReference } from './core/file-read.js';
|
|
14
|
+
export { FileStorage } from './core/file-storage.js';
|
|
15
|
+
export { isCalendarDate } from './core/formats/calendar-date-format.js';
|
|
16
|
+
export { declaredFormat, } from './core/formats/declared-format.js';
|
|
17
|
+
export { Int64Format } from './core/formats/int64-format.js';
|
|
18
|
+
export { LocalTimestampFormat } from './core/formats/local-timestamp-format.js';
|
|
19
|
+
export { isTimestamp } from './core/formats/timestamp-format.js';
|
|
20
|
+
export { Identifiers } from './core/identifiers.js';
|
|
21
|
+
export { Pipeline, PipelineError, WatchError, } from './core/pipeline.js';
|
|
22
|
+
export { readerCatalog } from './core/reader-relation.js';
|
|
23
|
+
export { reloadMode, } from './core/reload-mode.js';
|
|
24
|
+
export { StreamChangeError } from './core/stream-change.js';
|
|
25
|
+
export { validateRecords, } from './core/record-validation.js';
|
|
26
|
+
export { diffGroupedSnapshot, diffSnapshot, } from './core/snapshot.js';
|
|
27
|
+
export { Source, StreamStatus, } from './core/source.js';
|
|
28
|
+
export { Gauge } from './core/change-signal.js';
|
|
29
|
+
export { Stream } from './core/stream.js';
|
|
30
|
+
export { Target } from './core/target.js';
|
|
31
|
+
export { describeTarget, } from './core/target-description.js';
|
|
32
|
+
export { TargetOwnedError, Writer, } from './core/writer.js';
|
|
33
|
+
export { CheckpointStore, } from './state/checkpoint-store.js';
|
|
34
|
+
export { copyStatus, errorMessage, passError, passFailureType, passStatus, recordedFailure, SyncHistory, } from './state/sync-history.js';
|
|
35
|
+
export { syncHistoryRelations } from './state/sync-history-relations.js';
|
|
36
|
+
export { LocalFiles } from './storage/local-files.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type StoredCheckpoint = {
|
|
2
|
+
readonly binding: string;
|
|
3
|
+
readonly reloading: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type CheckpointSession = {
|
|
6
|
+
read(id: string): Promise<StoredCheckpoint | undefined>;
|
|
7
|
+
state(id: string): Promise<string | undefined>;
|
|
8
|
+
save(id: string, checkpoint: StoredCheckpoint & {
|
|
9
|
+
readonly state: string;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
reloaded(id: string): Promise<void>;
|
|
12
|
+
remove(id: string): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export type CheckpointBinding = {
|
|
15
|
+
readonly copy: object;
|
|
16
|
+
readonly shape: object;
|
|
17
|
+
};
|
|
18
|
+
export type CheckpointRun = {
|
|
19
|
+
state(id: string): Promise<unknown>;
|
|
20
|
+
reloading(id: string): boolean;
|
|
21
|
+
save(id: string, state: string, reloading: boolean): Promise<void>;
|
|
22
|
+
reloaded(id: string): Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
export declare abstract class CheckpointStore {
|
|
25
|
+
run<T>(bindings: ReadonlyMap<string, CheckpointBinding>, work: (run: CheckpointRun) => Promise<T>): Promise<T>;
|
|
26
|
+
reset(id: string): Promise<void>;
|
|
27
|
+
clear(id: string, drop: () => Promise<void>): Promise<void>;
|
|
28
|
+
protected abstract session<T>(ids: readonly string[], work: (session: CheckpointSession) => Promise<T>): Promise<T>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { StreamChangeError, changedDeclarations, } from '../core/stream-change.js';
|
|
3
|
+
// Checkpoints belong to a replication ID, not to a shared Stream or warehouse
|
|
4
|
+
// table. As Airbyte's platform keeps state apart from any destination, the
|
|
5
|
+
// orchestrator keeps them here, and saves each one the destination
|
|
6
|
+
// acknowledged, so a run that fails later resumes from its last commit.
|
|
7
|
+
export class CheckpointStore {
|
|
8
|
+
// Holds every replication's lock for the whole run.
|
|
9
|
+
async run(bindings, work) {
|
|
10
|
+
const ids = [...bindings.keys()].sort();
|
|
11
|
+
return this.session(ids, async (session) => {
|
|
12
|
+
const checkpoints = new Map();
|
|
13
|
+
for (const [id, { copy, shape }] of bindings) {
|
|
14
|
+
const binding = JSON.stringify([copy, shape]);
|
|
15
|
+
const saved = await session.read(id);
|
|
16
|
+
const [savedCopy, savedShape] = saved === undefined ? [] : JSON.parse(saved.binding);
|
|
17
|
+
const [currentCopy, currentShape] = JSON.parse(binding);
|
|
18
|
+
let refused;
|
|
19
|
+
if (saved !== undefined) {
|
|
20
|
+
const declarations = changedDeclarations(savedShape, currentShape);
|
|
21
|
+
if (!isDeepStrictEqual(savedCopy, currentCopy))
|
|
22
|
+
refused = new TypeError(`Checkpoint binding changed for ${id}; reset it or use a new copy ID`);
|
|
23
|
+
else if (declarations.length > 0)
|
|
24
|
+
refused = new StreamChangeError(id, declarations);
|
|
25
|
+
}
|
|
26
|
+
checkpoints.set(id, {
|
|
27
|
+
binding,
|
|
28
|
+
reloading: saved?.reloading ?? false,
|
|
29
|
+
refused,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const checkpoint = (id) => {
|
|
33
|
+
const found = checkpoints.get(id);
|
|
34
|
+
if (found === undefined)
|
|
35
|
+
throw new TypeError(`Checkpoint ${id} is not part of this run`);
|
|
36
|
+
if (found.refused !== undefined)
|
|
37
|
+
throw found.refused;
|
|
38
|
+
return found;
|
|
39
|
+
};
|
|
40
|
+
const durably = async (id, save) => {
|
|
41
|
+
try {
|
|
42
|
+
await save();
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
throw new Error(`Checkpoint ${id} was not saved after the destination committed; the next run replays from the last saved checkpoint`, { cause });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return work({
|
|
49
|
+
state: async (id) => {
|
|
50
|
+
checkpoint(id);
|
|
51
|
+
const state = await session.state(id);
|
|
52
|
+
return state === undefined ? null : JSON.parse(state);
|
|
53
|
+
},
|
|
54
|
+
reloading: (id) => checkpoint(id).reloading,
|
|
55
|
+
save: (id, state, reloading) => {
|
|
56
|
+
const { binding } = checkpoint(id);
|
|
57
|
+
return durably(id, () => session.save(id, { binding, reloading, state }));
|
|
58
|
+
},
|
|
59
|
+
reloaded: (id) => {
|
|
60
|
+
checkpoint(id);
|
|
61
|
+
return durably(id, () => session.reloaded(id));
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// Forgets progress but keeps the loaded rows: the next run reloads
|
|
67
|
+
// everything, as Airbyte's refresh that keeps records. It also accepts a
|
|
68
|
+
// stream whose declarations changed, keeping its rows under them.
|
|
69
|
+
async reset(id) {
|
|
70
|
+
await this.session([id], (session) => session.remove(id));
|
|
71
|
+
}
|
|
72
|
+
// Airbyte's Clear: removes the data, then the checkpoint, under the
|
|
73
|
+
// replication's lock. A crash between the two leaves a checkpoint beside an
|
|
74
|
+
// emptied target, and clear can simply run again.
|
|
75
|
+
async clear(id, drop) {
|
|
76
|
+
await this.session([id], async (session) => {
|
|
77
|
+
await drop();
|
|
78
|
+
await session.remove(id);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export declare const syncHistoryRelations: {
|
|
2
|
+
readonly sync_attempts: {
|
|
3
|
+
readonly name: 'sync_attempts';
|
|
4
|
+
readonly description: 'One row per pass of a connection. A run passes every stream the connection selected; a watch pass reads only the streams whose change signals changed, so an attempt vouches only for its own extraction_coverage rows. History is retained, including failures and unfinished attempts. A successful pass may write zero rows. A connection that stopped, a failed change signal or a failed upkeep records a failed attempt without outcomes. Setup/authentication before the pipeline is constructed is not observed. This records extraction/load outcomes, not subsequent mart publication.';
|
|
5
|
+
readonly columns: {
|
|
6
|
+
attempt_id: string;
|
|
7
|
+
connector: string;
|
|
8
|
+
source: string;
|
|
9
|
+
started_at: string;
|
|
10
|
+
completed_at: string;
|
|
11
|
+
status: string;
|
|
12
|
+
error: string;
|
|
13
|
+
failure_type: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
readonly extraction_coverage: {
|
|
17
|
+
readonly name: 'extraction_coverage';
|
|
18
|
+
readonly description: 'One row per selected stream per attempt, declared before extraction even when there are no records. Description and selection describe the requested export scope, not observed record dates or proof of upstream completeness. Only succeeded confirms that this copy completed that declared pass; partial/failed/running never establish complete coverage. Earlier successful declarations remain available. Raw target names identify storage, not reader grants; discover readable content in catalog.';
|
|
19
|
+
readonly columns: {
|
|
20
|
+
readonly attempt_id: string;
|
|
21
|
+
readonly connector: string;
|
|
22
|
+
readonly source: string;
|
|
23
|
+
readonly started_at: string;
|
|
24
|
+
readonly completed_at: 'Time the whole attempt was recorded as complete; NULL while unfinished. The copy may have finished earlier. Not record modification time or loaded_at.';
|
|
25
|
+
readonly stream: 'Source stream name, unique within one attempt.';
|
|
26
|
+
readonly target_schema: 'Schema of the raw destination table (main in SQLite); this metadata does not grant access to it.';
|
|
27
|
+
readonly target_table: 'Raw destination table name within target_schema.';
|
|
28
|
+
readonly target_exists: 'Whether the raw relation exists now, independent of this historical attempt. Existence does not prove successful extraction, current contents, or reader access.';
|
|
29
|
+
readonly sync_mode: 'Requested source mode: incremental or full_refresh. Incremental copies can resume saved state; the selection describes their configured scope, not every request made on this pass.';
|
|
30
|
+
readonly destination_sync_mode: 'Requested load mode, such as append_dedup. Partial passes can leave previously committed changes in the destination.';
|
|
31
|
+
readonly description: 'Connector-owned explanation of the scope, date boundaries, selection keys and source limitations for this stream.';
|
|
32
|
+
readonly selection: 'Structured configured selection, interpreted using description. Not computed from rows. Empty object means no additional configured selection, not unlimited upstream history.';
|
|
33
|
+
readonly status: 'running: no outcome recorded; succeeded: copy completed with no failures, even with zero changes; partial: failures after committed writes/deletes; failed: failures without committed row changes, or an error without outcomes; cancelled: the run was stopped on request before this copy ended, keeping what it committed. Consult failures for affected partitions.';
|
|
34
|
+
readonly written_count: 'Accepted record operations committed by this copy during this pass, including deduplication no-ops. Not changed-row or total-record counts. Zero is valid after success. NULL means no counts were reported.';
|
|
35
|
+
readonly deleted_count: 'Accepted deletion operations committed by this copy during this pass, including already-absent keys. Not a count of rows actually removed. NULL means no counts were reported.';
|
|
36
|
+
readonly failures: 'Array of {partition, error, failure_type, stacktrace} from pass outcomes. A null partition denotes a whole-stream or non-partition-specific failure; failure_type is config or system, as in sync_attempts. error is the message; stacktrace is the error with its stack and every cause it chains, such as the code of a refused connection behind "fetch failed". Empty array means none recorded; check status before assuming success.';
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
readonly sync_status: {
|
|
40
|
+
readonly name: 'sync_status';
|
|
41
|
+
readonly description: 'One row per connector with its latest started attempt, independently its most recently completed successful pass, and how current its data is. A watch pass covers only the streams that changed; see stream_status for each stream. A later failed, partial or unfinished pass does not erase prior success. Join extraction_coverage using the appropriate attempt ID: requested scope can change. NULL success means none recorded. Judge freshness by current_as_of: last_successful_sync_at measures sync completion, not service liveness or source completeness.';
|
|
42
|
+
readonly columns: {
|
|
43
|
+
readonly connector: string;
|
|
44
|
+
readonly latest_attempt_id: 'Most recently started attempt for this connector; join sync_attempts or extraction_coverage by attempt_id.';
|
|
45
|
+
readonly started_at: string;
|
|
46
|
+
readonly completed_at: string;
|
|
47
|
+
readonly status: string;
|
|
48
|
+
readonly error: string;
|
|
49
|
+
readonly failure_type: string;
|
|
50
|
+
readonly last_successful_attempt_id: 'Most recently completed all-copies-successful attempt; NULL if none. Its coverage may differ from the latest attempt.';
|
|
51
|
+
readonly last_successful_sync_at: 'Completion time of that successful pass, advanced even if no rows changed. NULL if no successful pass was recorded. Never inferred from loaded_at or record modification dates.';
|
|
52
|
+
readonly current_as_of: "The oldest current_as_of among the connector's streams in stream_status: everything it loaded matched its upstream at least as of this time. NULL until every stream has loaded once. An old value means its data is that old: no pass or watch has refreshed it since, a change failed to load, or the app that writes its upstream has not written it since, as WhatsApp while it is closed.";
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
readonly stream_status: {
|
|
56
|
+
readonly name: 'stream_status';
|
|
57
|
+
readonly description: 'One row per connector and stream: the latest attempt that declared the stream, independently the last attempt in which its copy succeeded, and how current the stream is. A watch pass reads only the streams its source reported changed, so a connector can succeed in sync_status while one of its streams last failed, and an unchanged stream keeps an old last_successful_sync_at while its watch keeps it current, up to when its source says its upstream was last current; judge a stream here, by current_as_of. NULL success means that stream never completed.';
|
|
58
|
+
readonly columns: {
|
|
59
|
+
readonly connector: string;
|
|
60
|
+
readonly stream: 'Source stream name.';
|
|
61
|
+
readonly target_schema: 'Schema of the raw destination table in the latest declaration (main in SQLite); this metadata does not grant access to it.';
|
|
62
|
+
readonly target_table: 'Raw destination table name within target_schema.';
|
|
63
|
+
readonly latest_attempt_id: 'Most recently started attempt that declared this stream; join extraction_coverage by attempt_id and stream.';
|
|
64
|
+
readonly started_at: string;
|
|
65
|
+
readonly completed_at: string;
|
|
66
|
+
readonly status: "This stream's copy status in the latest attempt: running, succeeded, partial, failed or cancelled. Other streams of the same attempt may differ.";
|
|
67
|
+
readonly last_successful_attempt_id: "Most recent completed attempt in which this stream's copy succeeded; NULL if none.";
|
|
68
|
+
readonly last_successful_sync_at: 'Completion time of that attempt, advanced even if no rows changed. NULL if this stream never succeeded. Never inferred from loaded_at or record modification dates.';
|
|
69
|
+
readonly current_as_of: 'When the stream last matched its upstream: when its last successful pass began reading, or a later check by a watch that found the stream unchanged since, whichever is later, and never later than when its source says the upstream itself was last current, as an app writes its store only while it runs. A watch records its checks about once a minute. NULL if the stream never succeeded. A stream whose latest change failed to load keeps the time before that change, so its age is how stale it is; compare it with the current time to judge freshness.';
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const attempt = {
|
|
2
|
+
attempt_id: 'Pass identifier. Join to extraction_coverage.attempt_id for the exact declarations and copy outcomes of this pass.',
|
|
3
|
+
connector: 'Connection name declared by the application. Each attempt is one pass: one read of the streams the connection selected.',
|
|
4
|
+
source: 'Source identity, not credentials or a record identifier.',
|
|
5
|
+
started_at: 'Database time when the attempt and its declared coverage were recorded, before the pass read anything. Not a source record modification or row load time.',
|
|
6
|
+
completed_at: 'Database time when the pass outcomes were recorded. NULL means no completion was recorded; this is not evidence that a process is alive.',
|
|
7
|
+
status: 'running: no completion recorded; succeeded: all selected copies completed, including empty or unchanged reads; partial: failures with some successful copies or committed writes/deletes; failed: failures without that progress; cancelled: the run was stopped on request, and error says why; what it committed before stays. No health of a watch is implied.',
|
|
8
|
+
error: 'Pipeline error message, or NULL after success or before completion. Per-copy failures and partitions are in extraction_coverage.',
|
|
9
|
+
failure_type: "Whose the failure is to fix, as the source classified it: config when at least one failure needs the user to change what they set up or granted, such as macOS access to the app's store; system for every other failure, which the user cannot fix by granting or configuring. NULL after success, before completion, or when cancelled.",
|
|
10
|
+
};
|
|
11
|
+
// How readers see the sync history: what each pass declared and loaded.
|
|
12
|
+
export const syncHistoryRelations = {
|
|
13
|
+
sync_attempts: {
|
|
14
|
+
name: 'sync_attempts',
|
|
15
|
+
description: 'One row per pass of a connection. A run passes every stream the connection selected; a watch pass reads only the streams whose change signals changed, so an attempt vouches only for its own extraction_coverage rows. History is retained, including failures and unfinished attempts. A successful pass may write zero rows. A connection that stopped, a failed change signal or a failed upkeep records a failed attempt without outcomes. Setup/authentication before the pipeline is constructed is not observed. This records extraction/load outcomes, not subsequent mart publication.',
|
|
16
|
+
columns: attempt,
|
|
17
|
+
},
|
|
18
|
+
extraction_coverage: {
|
|
19
|
+
name: 'extraction_coverage',
|
|
20
|
+
description: 'One row per selected stream per attempt, declared before extraction even when there are no records. Description and selection describe the requested export scope, not observed record dates or proof of upstream completeness. Only succeeded confirms that this copy completed that declared pass; partial/failed/running never establish complete coverage. Earlier successful declarations remain available. Raw target names identify storage, not reader grants; discover readable content in catalog.',
|
|
21
|
+
columns: {
|
|
22
|
+
attempt_id: attempt.attempt_id,
|
|
23
|
+
connector: attempt.connector,
|
|
24
|
+
source: attempt.source,
|
|
25
|
+
started_at: attempt.started_at,
|
|
26
|
+
completed_at: 'Time the whole attempt was recorded as complete; NULL while unfinished. The copy may have finished earlier. Not record modification time or loaded_at.',
|
|
27
|
+
stream: 'Source stream name, unique within one attempt.',
|
|
28
|
+
target_schema: 'Schema of the raw destination table (main in SQLite); this metadata does not grant access to it.',
|
|
29
|
+
target_table: 'Raw destination table name within target_schema.',
|
|
30
|
+
target_exists: 'Whether the raw relation exists now, independent of this historical attempt. Existence does not prove successful extraction, current contents, or reader access.',
|
|
31
|
+
sync_mode: 'Requested source mode: incremental or full_refresh. Incremental copies can resume saved state; the selection describes their configured scope, not every request made on this pass.',
|
|
32
|
+
destination_sync_mode: 'Requested load mode, such as append_dedup. Partial passes can leave previously committed changes in the destination.',
|
|
33
|
+
description: 'Connector-owned explanation of the scope, date boundaries, selection keys and source limitations for this stream.',
|
|
34
|
+
selection: 'Structured configured selection, interpreted using description. Not computed from rows. Empty object means no additional configured selection, not unlimited upstream history.',
|
|
35
|
+
status: 'running: no outcome recorded; succeeded: copy completed with no failures, even with zero changes; partial: failures after committed writes/deletes; failed: failures without committed row changes, or an error without outcomes; cancelled: the run was stopped on request before this copy ended, keeping what it committed. Consult failures for affected partitions.',
|
|
36
|
+
written_count: 'Accepted record operations committed by this copy during this pass, including deduplication no-ops. Not changed-row or total-record counts. Zero is valid after success. NULL means no counts were reported.',
|
|
37
|
+
deleted_count: 'Accepted deletion operations committed by this copy during this pass, including already-absent keys. Not a count of rows actually removed. NULL means no counts were reported.',
|
|
38
|
+
failures: 'Array of {partition, error, failure_type, stacktrace} from pass outcomes. A null partition denotes a whole-stream or non-partition-specific failure; failure_type is config or system, as in sync_attempts. error is the message; stacktrace is the error with its stack and every cause it chains, such as the code of a refused connection behind "fetch failed". Empty array means none recorded; check status before assuming success.',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
sync_status: {
|
|
42
|
+
name: 'sync_status',
|
|
43
|
+
description: 'One row per connector with its latest started attempt, independently its most recently completed successful pass, and how current its data is. A watch pass covers only the streams that changed; see stream_status for each stream. A later failed, partial or unfinished pass does not erase prior success. Join extraction_coverage using the appropriate attempt ID: requested scope can change. NULL success means none recorded. Judge freshness by current_as_of: last_successful_sync_at measures sync completion, not service liveness or source completeness.',
|
|
44
|
+
columns: {
|
|
45
|
+
connector: attempt.connector,
|
|
46
|
+
latest_attempt_id: 'Most recently started attempt for this connector; join sync_attempts or extraction_coverage by attempt_id.',
|
|
47
|
+
started_at: attempt.started_at,
|
|
48
|
+
completed_at: attempt.completed_at,
|
|
49
|
+
status: attempt.status,
|
|
50
|
+
error: attempt.error,
|
|
51
|
+
failure_type: attempt.failure_type,
|
|
52
|
+
last_successful_attempt_id: 'Most recently completed all-copies-successful attempt; NULL if none. Its coverage may differ from the latest attempt.',
|
|
53
|
+
last_successful_sync_at: 'Completion time of that successful pass, advanced even if no rows changed. NULL if no successful pass was recorded. Never inferred from loaded_at or record modification dates.',
|
|
54
|
+
current_as_of: "The oldest current_as_of among the connector's streams in stream_status: everything it loaded matched its upstream at least as of this time. NULL until every stream has loaded once. An old value means its data is that old: no pass or watch has refreshed it since, a change failed to load, or the app that writes its upstream has not written it since, as WhatsApp while it is closed.",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
stream_status: {
|
|
58
|
+
name: 'stream_status',
|
|
59
|
+
description: 'One row per connector and stream: the latest attempt that declared the stream, independently the last attempt in which its copy succeeded, and how current the stream is. A watch pass reads only the streams its source reported changed, so a connector can succeed in sync_status while one of its streams last failed, and an unchanged stream keeps an old last_successful_sync_at while its watch keeps it current, up to when its source says its upstream was last current; judge a stream here, by current_as_of. NULL success means that stream never completed.',
|
|
60
|
+
columns: {
|
|
61
|
+
connector: attempt.connector,
|
|
62
|
+
stream: 'Source stream name.',
|
|
63
|
+
target_schema: 'Schema of the raw destination table in the latest declaration (main in SQLite); this metadata does not grant access to it.',
|
|
64
|
+
target_table: 'Raw destination table name within target_schema.',
|
|
65
|
+
latest_attempt_id: 'Most recently started attempt that declared this stream; join extraction_coverage by attempt_id and stream.',
|
|
66
|
+
started_at: attempt.started_at,
|
|
67
|
+
completed_at: attempt.completed_at,
|
|
68
|
+
status: "This stream's copy status in the latest attempt: running, succeeded, partial, failed or cancelled. Other streams of the same attempt may differ.",
|
|
69
|
+
last_successful_attempt_id: "Most recent completed attempt in which this stream's copy succeeded; NULL if none.",
|
|
70
|
+
last_successful_sync_at: 'Completion time of that attempt, advanced even if no rows changed. NULL if this stream never succeeded. Never inferred from loaded_at or record modification dates.',
|
|
71
|
+
current_as_of: 'When the stream last matched its upstream: when its last successful pass began reading, or a later check by a watch that found the stream unchanged since, whichever is later, and never later than when its source says the upstream itself was last current, as an app writes its store only while it runs. A watch records its checks about once a minute. NULL if the stream never succeeded. A stream whose latest change failed to load keeps the time before that change, so its age is how stale it is; compare it with the current time to judge freshness.',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|