envio 3.5.0-alpha.2 → 3.5.0-rc.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/package.json +6 -6
- package/src/Batch.res +8 -8
- package/src/Batch.res.mjs +4 -3
- package/src/BatchProcessing.res +24 -0
- package/src/BatchProcessing.res.mjs +12 -0
- package/src/ChainFetching.res +20 -19
- package/src/ChainFetching.res.mjs +14 -14
- package/src/ChainId.res +58 -0
- package/src/ChainId.res.mjs +49 -0
- package/src/ChainId.resi +27 -0
- package/src/ChainMap.res +12 -26
- package/src/ChainMap.res.mjs +5 -41
- package/src/ChainMap.resi +5 -20
- package/src/ChainMetadata.res +1 -1
- package/src/ChainMetadata.res.mjs +3 -2
- package/src/ChainState.res +45 -24
- package/src/ChainState.res.mjs +40 -16
- package/src/ChainState.resi +3 -1
- package/src/Config.res +43 -26
- package/src/Config.res.mjs +19 -16
- package/src/ContractRegisterContext.res +1 -1
- package/src/ContractRegisterContext.res.mjs +1 -1
- package/src/Core.res +1 -0
- package/src/CrossChainState.res +90 -22
- package/src/CrossChainState.res.mjs +55 -16
- package/src/CrossChainState.resi +5 -1
- package/src/Env.res +0 -3
- package/src/Env.res.mjs +0 -3
- package/src/EventConfigBuilder.res +1 -1
- package/src/EventProcessing.res +4 -4
- package/src/EventProcessing.res.mjs +5 -5
- package/src/FetchState.res +489 -153
- package/src/FetchState.res.mjs +399 -129
- package/src/FinalizeBackfill.res +61 -0
- package/src/FinalizeBackfill.res.mjs +44 -0
- package/src/HandlerRegister.res +8 -8
- package/src/HandlerRegister.res.mjs +5 -4
- package/src/HandlerRegister.resi +1 -1
- package/src/InMemoryStore.res +4 -0
- package/src/InMemoryStore.res.mjs +1 -1
- package/src/InMemoryTable.res +15 -15
- package/src/InMemoryTable.res.mjs +14 -14
- package/src/IndexerLoop.res +9 -2
- package/src/IndexerLoop.res.mjs +8 -1
- package/src/IndexerState.res +52 -23
- package/src/IndexerState.res.mjs +52 -14
- package/src/IndexerState.resi +11 -6
- package/src/Internal.res +19 -17
- package/src/Internal.res.mjs +18 -9
- package/src/LoadLayer.res +10 -2
- package/src/LoadLayer.res.mjs +3 -1
- package/src/LogSelection.res +2 -2
- package/src/Main.res +7 -11
- package/src/Main.res.mjs +4 -6
- package/src/Metrics.res +6 -6
- package/src/Metrics.res.mjs +4 -3
- package/src/Persistence.res +23 -4
- package/src/PgStorage.res +447 -100
- package/src/PgStorage.res.mjs +285 -99
- package/src/RawEvent.res +1 -2
- package/src/RawEvent.res.mjs +1 -1
- package/src/Rollback.res +18 -20
- package/src/Rollback.res.mjs +9 -9
- package/src/RollbackCommit.res +2 -2
- package/src/RollbackCommit.res.mjs +3 -3
- package/src/SafeCheckpointTracking.res +2 -2
- package/src/SimulateDeadInputTracker.res +17 -21
- package/src/SimulateDeadInputTracker.res.mjs +9 -8
- package/src/SimulateItems.res +8 -10
- package/src/SimulateItems.res.mjs +9 -10
- package/src/Sink.res +2 -2
- package/src/Sink.res.mjs +3 -2
- package/src/TestIndexer.res +14 -14
- package/src/TestIndexer.res.mjs +16 -10
- package/src/UserContext.res +2 -2
- package/src/UserContext.res.mjs +2 -1
- package/src/Utils.res +0 -4
- package/src/Utils.res.mjs +1 -6
- package/src/bindings/ClickHouse.res +20 -2
- package/src/bindings/ClickHouse.res.mjs +25 -14
- package/src/db/EntityFilter.res +0 -4
- package/src/db/EntityFilter.res.mjs +1 -8
- package/src/db/EntityHistory.res +1 -1
- package/src/db/IndexCatalog.res +248 -0
- package/src/db/IndexCatalog.res.mjs +222 -0
- package/src/db/IndexDefinition.res +131 -0
- package/src/db/IndexDefinition.res.mjs +138 -0
- package/src/db/IndexManager.res +145 -0
- package/src/db/IndexManager.res.mjs +130 -0
- package/src/db/InternalTable.res +54 -23
- package/src/db/InternalTable.res.mjs +38 -23
- package/src/db/Table.res +24 -14
- package/src/db/Table.res.mjs +21 -16
- package/src/sources/AddressSet.res +5 -4
- package/src/sources/AddressStore.res +25 -14
- package/src/sources/AddressStore.res.mjs +10 -12
- package/src/sources/Evm.res +2 -2
- package/src/sources/Evm.res.mjs +1 -1
- package/src/sources/EvmChain.res +3 -3
- package/src/sources/EvmChain.res.mjs +3 -3
- package/src/sources/EvmHyperSyncSource.res +7 -7
- package/src/sources/EvmHyperSyncSource.res.mjs +6 -6
- package/src/sources/Fuel.res +2 -2
- package/src/sources/Fuel.res.mjs +1 -1
- package/src/sources/FuelHyperSyncClient.res +3 -0
- package/src/sources/FuelHyperSyncClient.res.mjs +1 -0
- package/src/sources/FuelHyperSyncSource.res +5 -7
- package/src/sources/FuelHyperSyncSource.res.mjs +5 -5
- package/src/sources/HyperSync.resi +2 -2
- package/src/sources/HyperSyncClient.res +5 -0
- package/src/sources/HyperSyncClient.res.mjs +1 -0
- package/src/sources/RpcSource.res +13 -14
- package/src/sources/RpcSource.res.mjs +13 -12
- package/src/sources/SimulateSource.res +42 -15
- package/src/sources/SimulateSource.res.mjs +27 -9
- package/src/sources/Source.res +1 -5
- package/src/sources/SourceManager.res +12 -12
- package/src/sources/SourceManager.res.mjs +11 -10
- package/src/sources/SourceManager.resi +2 -2
- package/src/sources/Svm.res +4 -5
- package/src/sources/Svm.res.mjs +5 -4
- package/src/sources/SvmHyperSyncClient.res +3 -0
- package/src/sources/SvmHyperSyncClient.res.mjs +1 -0
- package/src/sources/SvmHyperSyncSource.res +4 -4
- package/src/sources/SvmHyperSyncSource.res.mjs +3 -3
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +2 -1
- package/src/tui/components/CustomHooks.res +5 -4
- package/src/tui/components/CustomHooks.res.mjs +4 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Utils from "../Utils.res.mjs";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as IndexDefinition from "./IndexDefinition.res.mjs";
|
|
7
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
|
+
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
9
|
+
|
|
10
|
+
let rowsSchema = S$RescriptSchema.array(S$RescriptSchema.object(s => ({
|
|
11
|
+
tableName: s.f("tableName", S$RescriptSchema.string),
|
|
12
|
+
indexName: s.f("indexName", S$RescriptSchema.string),
|
|
13
|
+
method: s.f("method", S$RescriptSchema.string),
|
|
14
|
+
isValid: s.f("isValid", S$RescriptSchema.int),
|
|
15
|
+
isUnique: s.f("isUnique", S$RescriptSchema.int),
|
|
16
|
+
isPartial: s.f("isPartial", S$RescriptSchema.int),
|
|
17
|
+
isExpression: s.f("isExpression", S$RescriptSchema.int),
|
|
18
|
+
predicate: s.f("predicate", S$RescriptSchema.$$null(S$RescriptSchema.string)),
|
|
19
|
+
columns: s.f("columns", S$RescriptSchema.array(S$RescriptSchema.string)),
|
|
20
|
+
directions: s.f("directions", S$RescriptSchema.array(S$RescriptSchema.string))
|
|
21
|
+
})));
|
|
22
|
+
|
|
23
|
+
function makeQuery(pgSchema, indexName) {
|
|
24
|
+
return `SELECT
|
|
25
|
+
t.relname AS "tableName",
|
|
26
|
+
i.relname AS "indexName",
|
|
27
|
+
am.amname AS "method",
|
|
28
|
+
CASE WHEN ix.indisvalid AND ix.indisready THEN 1 ELSE 0 END AS "isValid",
|
|
29
|
+
CASE WHEN ix.indisunique THEN 1 ELSE 0 END AS "isUnique",
|
|
30
|
+
CASE WHEN ix.indpred IS NOT NULL THEN 1 ELSE 0 END AS "isPartial",
|
|
31
|
+
CASE WHEN ix.indexprs IS NOT NULL THEN 1 ELSE 0 END AS "isExpression",
|
|
32
|
+
pg_get_expr(ix.indpred, ix.indrelid, true) AS "predicate",
|
|
33
|
+
array_agg(
|
|
34
|
+
CASE WHEN k.attnum = 0
|
|
35
|
+
THEN pg_get_indexdef(ix.indexrelid, k.ord::int, true)
|
|
36
|
+
ELSE a.attname
|
|
37
|
+
END ORDER BY k.ord
|
|
38
|
+
) AS "columns",
|
|
39
|
+
array_agg(
|
|
40
|
+
CASE WHEN (ix.indoption[k.ord - 1] & 1) = 1 THEN 'DESC' ELSE 'ASC' END ORDER BY k.ord
|
|
41
|
+
) AS "directions"
|
|
42
|
+
FROM pg_index ix
|
|
43
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
44
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
45
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
46
|
+
JOIN pg_am am ON am.oid = i.relam
|
|
47
|
+
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON k.ord <= ix.indnkeyatts
|
|
48
|
+
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
49
|
+
WHERE n.nspname = '` + pgSchema + `'` + (
|
|
50
|
+
indexName !== undefined ? ` AND i.relname = '` + indexName + `'` : ""
|
|
51
|
+
) + `
|
|
52
|
+
GROUP BY t.relname, i.relname, am.amname, ix.indisvalid, ix.indisready,
|
|
53
|
+
ix.indisunique, ix.indpred, ix.indrelid, (ix.indexprs IS NOT NULL);`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fromRow(row) {
|
|
57
|
+
return {
|
|
58
|
+
name: row.indexName,
|
|
59
|
+
tableName: row.tableName,
|
|
60
|
+
method: row.method,
|
|
61
|
+
columns: row.columns.map((name, idx) => {
|
|
62
|
+
let match = row.directions[idx];
|
|
63
|
+
let tmp;
|
|
64
|
+
tmp = match === "DESC" ? "Desc" : "Asc";
|
|
65
|
+
return {
|
|
66
|
+
name: name,
|
|
67
|
+
direction: tmp
|
|
68
|
+
};
|
|
69
|
+
}),
|
|
70
|
+
isValid: row.isValid === 1,
|
|
71
|
+
isUnique: row.isUnique === 1,
|
|
72
|
+
isPartial: row.isPartial === 1,
|
|
73
|
+
isExpression: row.isExpression === 1,
|
|
74
|
+
predicate: row.predicate
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function leadsWith(entry, definition) {
|
|
79
|
+
if (definition.columns.length <= entry.columns.length) {
|
|
80
|
+
return definition.columns.every((column, idx) => {
|
|
81
|
+
let actual = entry.columns[idx];
|
|
82
|
+
if (actual !== undefined && actual.name === column.name) {
|
|
83
|
+
return actual.direction === column.direction;
|
|
84
|
+
} else {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function coverageKey(coverage) {
|
|
94
|
+
if (coverage === "Exact") {
|
|
95
|
+
return "=";
|
|
96
|
+
} else {
|
|
97
|
+
return "^";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rejectReason(entry, definition, coverage) {
|
|
102
|
+
if (entry.tableName !== definition.tableName) {
|
|
103
|
+
return `it is on table "` + entry.tableName + `"`;
|
|
104
|
+
} else if (entry.isValid) {
|
|
105
|
+
if (entry.isPartial) {
|
|
106
|
+
return `it is partial (WHERE ` + Stdlib_Option.getOr(entry.predicate, "...") + `), so it only covers part of the table`;
|
|
107
|
+
} else if (entry.isExpression) {
|
|
108
|
+
return "it indexes an expression rather than plain columns";
|
|
109
|
+
} else if (entry.method !== definition.method) {
|
|
110
|
+
return `it uses the ` + entry.method + ` access method, not ` + definition.method;
|
|
111
|
+
} else if (leadsWith(entry, definition)) {
|
|
112
|
+
if (coverage === "Exact" && entry.columns.length !== definition.columns.length) {
|
|
113
|
+
return `it covers (` + entry.columns.map(IndexDefinition.columnKey).join(", ") + `) rather than exactly the declared columns`;
|
|
114
|
+
} else {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
return `it covers (` + entry.columns.map(IndexDefinition.columnKey).join(", ") + `) instead of leading with the requested columns`;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
return "PostgreSQL reports it as invalid or not ready";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function satisfies(entry, definition, coverage) {
|
|
126
|
+
return Stdlib_Option.isNone(rejectReason(entry, definition, coverage));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isExactly(entry, definition) {
|
|
130
|
+
if (entry.tableName === definition.tableName && entry.method === definition.method && !entry.isUnique && !entry.isPartial && !entry.isExpression && entry.columns.length === definition.columns.length) {
|
|
131
|
+
return leadsWith(entry, definition);
|
|
132
|
+
} else {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function make() {
|
|
138
|
+
return {
|
|
139
|
+
byName: {},
|
|
140
|
+
covering: {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function set(catalog, entry) {
|
|
145
|
+
catalog.byName[entry.name] = entry;
|
|
146
|
+
catalog.covering = {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function remove(catalog, name) {
|
|
150
|
+
Utils.Dict.deleteInPlace(catalog.byName, name);
|
|
151
|
+
catalog.covering = {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fromRows(rows) {
|
|
155
|
+
let catalog = {
|
|
156
|
+
byName: {},
|
|
157
|
+
covering: {}
|
|
158
|
+
};
|
|
159
|
+
rows.forEach(row => set(catalog, fromRow(row)));
|
|
160
|
+
return catalog;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getByName(catalog, name) {
|
|
164
|
+
return catalog.byName[name];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function entries(catalog) {
|
|
168
|
+
return Object.values(catalog.byName);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function size(catalog) {
|
|
172
|
+
return Object.keys(catalog.byName).length;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let nothingCovers = "";
|
|
176
|
+
|
|
177
|
+
function find(catalog, definition, coverage) {
|
|
178
|
+
let cacheKey = coverageKey(coverage) + IndexDefinition.key(definition);
|
|
179
|
+
let name = catalog.covering[cacheKey];
|
|
180
|
+
if (name !== undefined) {
|
|
181
|
+
if (name === nothingCovers) {
|
|
182
|
+
return;
|
|
183
|
+
} else {
|
|
184
|
+
return catalog.byName[name];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let found = Object.values(catalog.byName).find(entry => satisfies(entry, definition, coverage));
|
|
188
|
+
catalog.covering[cacheKey] = found !== undefined ? found.name : nothingCovers;
|
|
189
|
+
return found;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function invalidNames(catalog) {
|
|
193
|
+
return Stdlib_Array.filterMap(Object.values(catalog.byName), entry => {
|
|
194
|
+
if (entry.isValid) {
|
|
195
|
+
return;
|
|
196
|
+
} else {
|
|
197
|
+
return entry.name;
|
|
198
|
+
}
|
|
199
|
+
}).toSorted(Primitive_string.compare);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
rowsSchema,
|
|
204
|
+
makeQuery,
|
|
205
|
+
fromRow,
|
|
206
|
+
leadsWith,
|
|
207
|
+
coverageKey,
|
|
208
|
+
rejectReason,
|
|
209
|
+
satisfies,
|
|
210
|
+
isExactly,
|
|
211
|
+
make,
|
|
212
|
+
set,
|
|
213
|
+
remove,
|
|
214
|
+
fromRows,
|
|
215
|
+
getByName,
|
|
216
|
+
entries,
|
|
217
|
+
size,
|
|
218
|
+
nothingCovers,
|
|
219
|
+
find,
|
|
220
|
+
invalidNames,
|
|
221
|
+
}
|
|
222
|
+
/* rowsSchema Not a pure module */
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// What the indexer wants an index to be: a table, its ordered key columns with
|
|
2
|
+
// their directions, and an access method. That tuple is the index's identity;
|
|
3
|
+
// the name is derived from it rather than being part of it, so the catalog can
|
|
4
|
+
// always be matched on what an index actually covers.
|
|
5
|
+
|
|
6
|
+
let btree = "btree"
|
|
7
|
+
|
|
8
|
+
type column = {
|
|
9
|
+
name: string,
|
|
10
|
+
direction: Table.indexFieldDirection,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type t = {
|
|
14
|
+
tableName: string,
|
|
15
|
+
columns: array<column>,
|
|
16
|
+
method: string,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let make = (~tableName, ~columns, ~method=btree) => {tableName, columns, method}
|
|
20
|
+
|
|
21
|
+
let single = (~tableName, ~column) =>
|
|
22
|
+
make(~tableName, ~columns=[{name: column, direction: Table.Asc}])
|
|
23
|
+
|
|
24
|
+
let fromIndexFields = (~tableName, ~indexFields: array<Table.compositeIndexField>) =>
|
|
25
|
+
make(
|
|
26
|
+
~tableName,
|
|
27
|
+
~columns=indexFields->Array.map(({fieldName, direction}) => {name: fieldName, direction}),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
let columnKey = ({name, direction}) =>
|
|
31
|
+
switch direction {
|
|
32
|
+
| Table.Asc => name
|
|
33
|
+
| Desc => `${name} DESC`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let key = ({tableName, columns, method}) =>
|
|
37
|
+
`${tableName}|${method}|${columns->Array.map(columnKey)->Array.joinUnsafe(",")}`
|
|
38
|
+
|
|
39
|
+
let describe = definition =>
|
|
40
|
+
`${definition.tableName}(${definition.columns
|
|
41
|
+
->Array.map(columnKey)
|
|
42
|
+
->Array.joinUnsafe(", ")}) using ${definition.method}`
|
|
43
|
+
|
|
44
|
+
@scope("Math") @val external imul: (int, int) => int = "imul"
|
|
45
|
+
|
|
46
|
+
// 2166136261 as a signed 32-bit int.
|
|
47
|
+
let fnvOffsetBasis = -2128831035
|
|
48
|
+
let fnvPrime = 16777619
|
|
49
|
+
|
|
50
|
+
let fnv1a = (input, ~seed) => {
|
|
51
|
+
let hash = ref(seed)
|
|
52
|
+
for idx in 0 to input->String.length - 1 {
|
|
53
|
+
hash := imul(hash.contents->Int.bitwiseXor(input->String.charCodeAtUnsafe(idx)), fnvPrime)
|
|
54
|
+
}
|
|
55
|
+
hash.contents
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let hashLength = 10
|
|
59
|
+
|
|
60
|
+
let toBase36 = (value, ~length) => value->Int.toString(~radix=36)->String.padStart(length, "0")
|
|
61
|
+
|
|
62
|
+
// 50 bits of the identity, base36-encoded. Two indexes sharing a hash would
|
|
63
|
+
// share an identifier, so the width matters: 50 bits is far past the number of
|
|
64
|
+
// indexes a schema can hold, while still fitting in 10 characters. The halves
|
|
65
|
+
// are encoded separately because combining them would overflow the 32-bit
|
|
66
|
+
// integer arithmetic the shifts compile to.
|
|
67
|
+
let identityHash = definition => {
|
|
68
|
+
let key = definition->key
|
|
69
|
+
let low = fnv1a(key, ~seed=fnvOffsetBasis)->Int.shiftRightUnsigned(2)
|
|
70
|
+
let high =
|
|
71
|
+
fnv1a(key, ~seed=fnvOffsetBasis->Int.bitwiseXor(0x27d4eb2f))->Int.shiftRightUnsigned(12)
|
|
72
|
+
toBase36(low, ~length=6) ++ toBase36(high, ~length=4)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let directionSuffix = (direction: Table.indexFieldDirection) =>
|
|
76
|
+
switch direction {
|
|
77
|
+
| Asc => ""
|
|
78
|
+
| Desc => "_desc"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `<Entity>_<column>`, with each further column appended in order. Only there so
|
|
82
|
+
// a human reading `\d` output can tell what the index is for.
|
|
83
|
+
let readablePrefix = ({tableName, columns}) =>
|
|
84
|
+
tableName ++
|
|
85
|
+
"_" ++
|
|
86
|
+
columns
|
|
87
|
+
->Array.map(({name, direction}) => name ++ directionSuffix(direction))
|
|
88
|
+
->Array.joinUnsafe("_")
|
|
89
|
+
|
|
90
|
+
let pgMaxIdentifierLength = 63
|
|
91
|
+
let maxPrefixLength = pgMaxIdentifierLength - hashLength - 1
|
|
92
|
+
|
|
93
|
+
// Postgres truncates identifiers past 63 bytes on its own, and two long field
|
|
94
|
+
// names would then collapse onto one name. Truncating only the readable half
|
|
95
|
+
// and keeping the hash whole makes every generated name distinct by
|
|
96
|
+
// construction. Prefixes are ASCII (GraphQL names are), so characters and
|
|
97
|
+
// Postgres' byte limit line up.
|
|
98
|
+
let name = definition => {
|
|
99
|
+
let prefix = definition->readablePrefix
|
|
100
|
+
let prefix =
|
|
101
|
+
prefix->String.length > maxPrefixLength
|
|
102
|
+
? prefix->String.slice(~start=0, ~end=maxPrefixLength)
|
|
103
|
+
: prefix
|
|
104
|
+
`${prefix}_${definition->identityHash}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let directionSql = (direction: Table.indexFieldDirection) =>
|
|
108
|
+
switch direction {
|
|
109
|
+
| Asc => ""
|
|
110
|
+
| Desc => " DESC"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let columnsSql = ({columns}) =>
|
|
114
|
+
columns
|
|
115
|
+
->Array.map(({name, direction}) => `"${name}"${directionSql(direction)}`)
|
|
116
|
+
->Array.joinUnsafe(", ")
|
|
117
|
+
|
|
118
|
+
// Plain DDL, not CONCURRENTLY: it builds from a single table scan instead of
|
|
119
|
+
// two, and the SHARE lock it takes blocks writes but not reads, so queries keep
|
|
120
|
+
// being served while it runs. The indexer is the only writer, and every caller
|
|
121
|
+
// is happy to wait on it.
|
|
122
|
+
//
|
|
123
|
+
// No `IF NOT EXISTS` either — a skipped create is indistinguishable from a
|
|
124
|
+
// successful one, and the whole point of the generated name is that nothing
|
|
125
|
+
// else can hold it.
|
|
126
|
+
let makeCreateQuery = (definition, ~pgSchema) => {
|
|
127
|
+
let using = definition.method === btree ? "" : ` USING ${definition.method}`
|
|
128
|
+
`CREATE INDEX "${definition->name}" ON "${pgSchema}"."${definition.tableName}"${using}(${definition->columnsSql});`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let makeDropQuery = (~pgSchema, ~indexName) => `DROP INDEX IF EXISTS "${pgSchema}"."${indexName}";`
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let btree = "btree";
|
|
5
|
+
|
|
6
|
+
function make(tableName, columns, methodOpt) {
|
|
7
|
+
let method = methodOpt !== undefined ? methodOpt : btree;
|
|
8
|
+
return {
|
|
9
|
+
tableName: tableName,
|
|
10
|
+
columns: columns,
|
|
11
|
+
method: method
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function single(tableName, column) {
|
|
16
|
+
return make(tableName, [{
|
|
17
|
+
name: column,
|
|
18
|
+
direction: "Asc"
|
|
19
|
+
}], undefined);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fromIndexFields(tableName, indexFields) {
|
|
23
|
+
return make(tableName, indexFields.map(param => ({
|
|
24
|
+
name: param.fieldName,
|
|
25
|
+
direction: param.direction
|
|
26
|
+
})), undefined);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function columnKey(param) {
|
|
30
|
+
let name = param.name;
|
|
31
|
+
if (param.direction === "Asc") {
|
|
32
|
+
return name;
|
|
33
|
+
} else {
|
|
34
|
+
return name + ` DESC`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function key(param) {
|
|
39
|
+
return param.tableName + `|` + param.method + `|` + param.columns.map(columnKey).join(",");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function describe(definition) {
|
|
43
|
+
return definition.tableName + `(` + definition.columns.map(columnKey).join(", ") + `) using ` + definition.method;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fnv1a(input, seed) {
|
|
47
|
+
let hash = seed;
|
|
48
|
+
for (let idx = 0, idx_finish = input.length; idx < idx_finish; ++idx) {
|
|
49
|
+
hash = Math.imul(hash ^ input.charCodeAt(idx), 16777619);
|
|
50
|
+
}
|
|
51
|
+
return hash;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toBase36(value, length) {
|
|
55
|
+
return value.toString(36).padStart(length, "0");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function identityHash(definition) {
|
|
59
|
+
let key$1 = key(definition);
|
|
60
|
+
let low = (fnv1a(key$1, -2128831035) >>> 2);
|
|
61
|
+
let high = (fnv1a(key$1, -1496811798) >>> 12);
|
|
62
|
+
return toBase36(low, 6) + toBase36(high, 4);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function directionSuffix(direction) {
|
|
66
|
+
if (direction === "Asc") {
|
|
67
|
+
return "";
|
|
68
|
+
} else {
|
|
69
|
+
return "_desc";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readablePrefix(param) {
|
|
74
|
+
return param.tableName + "_" + param.columns.map(param => param.name + directionSuffix(param.direction)).join("_");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let maxPrefixLength = 52;
|
|
78
|
+
|
|
79
|
+
function name(definition) {
|
|
80
|
+
let prefix = readablePrefix(definition);
|
|
81
|
+
let prefix$1 = prefix.length > maxPrefixLength ? prefix.slice(0, maxPrefixLength) : prefix;
|
|
82
|
+
return prefix$1 + `_` + identityHash(definition);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function directionSql(direction) {
|
|
86
|
+
if (direction === "Asc") {
|
|
87
|
+
return "";
|
|
88
|
+
} else {
|
|
89
|
+
return " DESC";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function columnsSql(param) {
|
|
94
|
+
return param.columns.map(param => `"` + param.name + `"` + directionSql(param.direction)).join(", ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function makeCreateQuery(definition, pgSchema) {
|
|
98
|
+
let using = definition.method === btree ? "" : ` USING ` + definition.method;
|
|
99
|
+
return `CREATE INDEX "` + name(definition) + `" ON "` + pgSchema + `"."` + definition.tableName + `"` + using + `(` + columnsSql(definition) + `);`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function makeDropQuery(pgSchema, indexName) {
|
|
103
|
+
return `DROP INDEX IF EXISTS "` + pgSchema + `"."` + indexName + `";`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let fnvOffsetBasis = -2128831035;
|
|
107
|
+
|
|
108
|
+
let fnvPrime = 16777619;
|
|
109
|
+
|
|
110
|
+
let hashLength = 10;
|
|
111
|
+
|
|
112
|
+
let pgMaxIdentifierLength = 63;
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
btree,
|
|
116
|
+
make,
|
|
117
|
+
single,
|
|
118
|
+
fromIndexFields,
|
|
119
|
+
columnKey,
|
|
120
|
+
key,
|
|
121
|
+
describe,
|
|
122
|
+
fnvOffsetBasis,
|
|
123
|
+
fnvPrime,
|
|
124
|
+
fnv1a,
|
|
125
|
+
hashLength,
|
|
126
|
+
toBase36,
|
|
127
|
+
identityHash,
|
|
128
|
+
directionSuffix,
|
|
129
|
+
readablePrefix,
|
|
130
|
+
pgMaxIdentifierLength,
|
|
131
|
+
maxPrefixLength,
|
|
132
|
+
name,
|
|
133
|
+
directionSql,
|
|
134
|
+
columnsSql,
|
|
135
|
+
makeCreateQuery,
|
|
136
|
+
makeDropQuery,
|
|
137
|
+
}
|
|
138
|
+
/* No side effect */
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Decides what has to happen for a desired index to exist, serialises the work
|
|
2
|
+
// so two builds never fight over the same table, and refuses to record an index
|
|
3
|
+
// as present until Postgres has been asked again what it actually built.
|
|
4
|
+
|
|
5
|
+
// A build the catalog doesn't already cover. `isRebuild` means an unusable
|
|
6
|
+
// index holds the generated name with exactly our identity, so it can only be
|
|
7
|
+
// one of ours — dropped and built again rather than left to block the name.
|
|
8
|
+
type prepared = {
|
|
9
|
+
definition: IndexDefinition.t,
|
|
10
|
+
name: string,
|
|
11
|
+
isRebuild: bool,
|
|
12
|
+
queries: array<string>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type t = {
|
|
16
|
+
mutable catalog: IndexCatalog.t,
|
|
17
|
+
// Keyed by index identity and coverage so concurrent identical requests
|
|
18
|
+
// await one build.
|
|
19
|
+
inflight: dict<promise<unit>>,
|
|
20
|
+
// Tail of the build chain per table, so two different indexes on the same
|
|
21
|
+
// table are built one after the other rather than at once.
|
|
22
|
+
tableQueue: dict<promise<unit>>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let make = () => {
|
|
26
|
+
catalog: IndexCatalog.make(),
|
|
27
|
+
inflight: Dict.make(),
|
|
28
|
+
tableQueue: Dict.make(),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let reload = (manager, ~rows) => {
|
|
32
|
+
manager.catalog = IndexCatalog.fromRows(~rows)
|
|
33
|
+
manager.catalog
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let catalog = manager => manager.catalog
|
|
37
|
+
|
|
38
|
+
let isSatisfied = (manager, definition, ~coverage) =>
|
|
39
|
+
manager.catalog->IndexCatalog.find(definition, ~coverage)->Option.isSome
|
|
40
|
+
|
|
41
|
+
// The name is derived from a hash of the full identity, so an index holding it
|
|
42
|
+
// was generated for this exact identity. Anything else under that name is
|
|
43
|
+
// someone else's, and guessing which is worse than stopping.
|
|
44
|
+
let conflictError = (~entry: IndexCatalog.entry, ~definition, ~pgSchema) =>
|
|
45
|
+
Utils.Error.make(
|
|
46
|
+
`Cannot create the index "${entry.name}" in schema "${pgSchema}" for ${definition->IndexDefinition.describe}. A different index already holds that name and the indexer can't safely replace it: ${entry
|
|
47
|
+
->IndexCatalog.rejectReason(definition, ~coverage=IndexCatalog.Exact)
|
|
48
|
+
->Option.getOr("it is unique")}. Drop that index by hand, then restart the indexer.`,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
let verificationError = (~name, ~definition, ~pgSchema, ~reason) =>
|
|
52
|
+
Utils.Error.make(
|
|
53
|
+
`The index "${name}" in schema "${pgSchema}" is not usable after its DDL ran: ${reason}. It was meant to cover ${definition->IndexDefinition.describe}. Drop it by hand, then restart the indexer.`,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// `None` when the catalog already covers the definition. Resolving this before
|
|
57
|
+
// any DDL runs means a name held by an unrelated index fails while nothing is
|
|
58
|
+
// half-built.
|
|
59
|
+
let prepare = (manager, ~definition, ~coverage, ~pgSchema): option<prepared> =>
|
|
60
|
+
switch manager.catalog->IndexCatalog.find(definition, ~coverage) {
|
|
61
|
+
| Some(_) => None
|
|
62
|
+
| None =>
|
|
63
|
+
let name = definition->IndexDefinition.name
|
|
64
|
+
let create = definition->IndexDefinition.makeCreateQuery(~pgSchema)
|
|
65
|
+
switch manager.catalog->IndexCatalog.getByName(name) {
|
|
66
|
+
| None => Some({definition, name, isRebuild: false, queries: [create]})
|
|
67
|
+
| Some(entry) if entry->IndexCatalog.isExactly(definition) =>
|
|
68
|
+
Some({
|
|
69
|
+
definition,
|
|
70
|
+
name,
|
|
71
|
+
isRebuild: true,
|
|
72
|
+
queries: [IndexDefinition.makeDropQuery(~pgSchema, ~indexName=name), create],
|
|
73
|
+
})
|
|
74
|
+
| Some(entry) => throw(conflictError(~entry, ~definition, ~pgSchema))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The DDL succeeding is not proof the index exists in a form the planner will
|
|
79
|
+
// use: a build can leave an index INVALID, and a name we assumed free would
|
|
80
|
+
// have made it a no-op. `rows` is a fresh read of that one index from
|
|
81
|
+
// pg_catalog — inside the same transaction when there is one, so a failure
|
|
82
|
+
// rolls the DDL back with it.
|
|
83
|
+
let verifyOrThrow = (prepared, ~rows, ~pgSchema): IndexCatalog.entry => {
|
|
84
|
+
let {definition, name} = prepared
|
|
85
|
+
switch rows->Array.map(IndexCatalog.fromRow)->Array.find(entry => entry.name === name) {
|
|
86
|
+
| None =>
|
|
87
|
+
throw(verificationError(~name, ~definition, ~pgSchema, ~reason="PostgreSQL has no such index"))
|
|
88
|
+
| Some(entry) =>
|
|
89
|
+
// The build is verified against `Exact` whatever the request asked for: we
|
|
90
|
+
// just created this index from the definition, so anything short of a
|
|
91
|
+
// byte-for-byte match means Postgres built something else.
|
|
92
|
+
switch entry->IndexCatalog.rejectReason(definition, ~coverage=IndexCatalog.Exact) {
|
|
93
|
+
| Some(reason) => throw(verificationError(~name, ~definition, ~pgSchema, ~reason))
|
|
94
|
+
| None => entry
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Applied only once the DDL is durable — inside a transaction that means after
|
|
100
|
+
// the commit, so a rollback can't leave the catalog claiming an index the
|
|
101
|
+
// schema doesn't have.
|
|
102
|
+
let record = (manager, entry) => manager.catalog->IndexCatalog.set(entry)
|
|
103
|
+
|
|
104
|
+
// Puts one index back in step with the database. A build that commits its DDL
|
|
105
|
+
// and then fails to read it back would otherwise leave the catalog claiming the
|
|
106
|
+
// name is free, and every later attempt would replan a create and hit
|
|
107
|
+
// "relation already exists" for the rest of the run.
|
|
108
|
+
let resync = (manager, ~name, ~rows) =>
|
|
109
|
+
switch rows->Array.map(IndexCatalog.fromRow)->Array.find(entry => entry.name === name) {
|
|
110
|
+
| Some(entry) => manager.catalog->IndexCatalog.set(entry)
|
|
111
|
+
| None => manager.catalog->IndexCatalog.remove(name)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Runs `build` unless the catalog already covers the definition. Nothing is
|
|
115
|
+
// recorded here: `build` owns the verification, so a failed or unverified build
|
|
116
|
+
// leaves the manager untouched and the next request retries.
|
|
117
|
+
let ensure = (manager, ~definition: IndexDefinition.t, ~coverage, ~build) =>
|
|
118
|
+
if manager->isSatisfied(definition, ~coverage) {
|
|
119
|
+
Promise.resolve()
|
|
120
|
+
} else {
|
|
121
|
+
// Keyed by coverage as well as identity: an `Exact` request joining an
|
|
122
|
+
// in-flight `LeadingColumns` one would resolve as soon as that build
|
|
123
|
+
// decided a leading composite already served it, and the declared index
|
|
124
|
+
// would never be created.
|
|
125
|
+
let key = `${coverage->IndexCatalog.coverageKey}${definition->IndexDefinition.key}`
|
|
126
|
+
switch manager.inflight->Utils.Dict.dangerouslyGetNonOption(key) {
|
|
127
|
+
| Some(promise) => promise
|
|
128
|
+
| None =>
|
|
129
|
+
let tail = switch manager.tableQueue->Utils.Dict.dangerouslyGetNonOption(
|
|
130
|
+
definition.tableName,
|
|
131
|
+
) {
|
|
132
|
+
| Some(tail) => tail
|
|
133
|
+
| None => Promise.resolve()
|
|
134
|
+
}
|
|
135
|
+
let promise = tail
|
|
136
|
+
// The queued build may have been satisfied while waiting behind another
|
|
137
|
+
// one on the same table (eg a finalize pass created it).
|
|
138
|
+
->Promise.then(() =>
|
|
139
|
+
manager->isSatisfied(definition, ~coverage) ? Promise.resolve() : build()
|
|
140
|
+
)
|
|
141
|
+
manager.inflight->Dict.set(key, promise)
|
|
142
|
+
manager.tableQueue->Dict.set(definition.tableName, promise->Utils.Promise.silentCatch)
|
|
143
|
+
promise->Promise.finally(() => manager.inflight->Utils.Dict.deleteInPlace(key))
|
|
144
|
+
}
|
|
145
|
+
}
|