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.
Files changed (129) hide show
  1. package/package.json +6 -6
  2. package/src/Batch.res +8 -8
  3. package/src/Batch.res.mjs +4 -3
  4. package/src/BatchProcessing.res +24 -0
  5. package/src/BatchProcessing.res.mjs +12 -0
  6. package/src/ChainFetching.res +20 -19
  7. package/src/ChainFetching.res.mjs +14 -14
  8. package/src/ChainId.res +58 -0
  9. package/src/ChainId.res.mjs +49 -0
  10. package/src/ChainId.resi +27 -0
  11. package/src/ChainMap.res +12 -26
  12. package/src/ChainMap.res.mjs +5 -41
  13. package/src/ChainMap.resi +5 -20
  14. package/src/ChainMetadata.res +1 -1
  15. package/src/ChainMetadata.res.mjs +3 -2
  16. package/src/ChainState.res +45 -24
  17. package/src/ChainState.res.mjs +40 -16
  18. package/src/ChainState.resi +3 -1
  19. package/src/Config.res +43 -26
  20. package/src/Config.res.mjs +19 -16
  21. package/src/ContractRegisterContext.res +1 -1
  22. package/src/ContractRegisterContext.res.mjs +1 -1
  23. package/src/Core.res +1 -0
  24. package/src/CrossChainState.res +90 -22
  25. package/src/CrossChainState.res.mjs +55 -16
  26. package/src/CrossChainState.resi +5 -1
  27. package/src/Env.res +0 -3
  28. package/src/Env.res.mjs +0 -3
  29. package/src/EventConfigBuilder.res +1 -1
  30. package/src/EventProcessing.res +4 -4
  31. package/src/EventProcessing.res.mjs +5 -5
  32. package/src/FetchState.res +489 -153
  33. package/src/FetchState.res.mjs +399 -129
  34. package/src/FinalizeBackfill.res +61 -0
  35. package/src/FinalizeBackfill.res.mjs +44 -0
  36. package/src/HandlerRegister.res +8 -8
  37. package/src/HandlerRegister.res.mjs +5 -4
  38. package/src/HandlerRegister.resi +1 -1
  39. package/src/InMemoryStore.res +4 -0
  40. package/src/InMemoryStore.res.mjs +1 -1
  41. package/src/InMemoryTable.res +15 -15
  42. package/src/InMemoryTable.res.mjs +14 -14
  43. package/src/IndexerLoop.res +9 -2
  44. package/src/IndexerLoop.res.mjs +8 -1
  45. package/src/IndexerState.res +52 -23
  46. package/src/IndexerState.res.mjs +52 -14
  47. package/src/IndexerState.resi +11 -6
  48. package/src/Internal.res +19 -17
  49. package/src/Internal.res.mjs +18 -9
  50. package/src/LoadLayer.res +10 -2
  51. package/src/LoadLayer.res.mjs +3 -1
  52. package/src/LogSelection.res +2 -2
  53. package/src/Main.res +7 -11
  54. package/src/Main.res.mjs +4 -6
  55. package/src/Metrics.res +6 -6
  56. package/src/Metrics.res.mjs +4 -3
  57. package/src/Persistence.res +23 -4
  58. package/src/PgStorage.res +447 -100
  59. package/src/PgStorage.res.mjs +285 -99
  60. package/src/RawEvent.res +1 -2
  61. package/src/RawEvent.res.mjs +1 -1
  62. package/src/Rollback.res +18 -20
  63. package/src/Rollback.res.mjs +9 -9
  64. package/src/RollbackCommit.res +2 -2
  65. package/src/RollbackCommit.res.mjs +3 -3
  66. package/src/SafeCheckpointTracking.res +2 -2
  67. package/src/SimulateDeadInputTracker.res +17 -21
  68. package/src/SimulateDeadInputTracker.res.mjs +9 -8
  69. package/src/SimulateItems.res +8 -10
  70. package/src/SimulateItems.res.mjs +9 -10
  71. package/src/Sink.res +2 -2
  72. package/src/Sink.res.mjs +3 -2
  73. package/src/TestIndexer.res +14 -14
  74. package/src/TestIndexer.res.mjs +16 -10
  75. package/src/UserContext.res +2 -2
  76. package/src/UserContext.res.mjs +2 -1
  77. package/src/Utils.res +0 -4
  78. package/src/Utils.res.mjs +1 -6
  79. package/src/bindings/ClickHouse.res +20 -2
  80. package/src/bindings/ClickHouse.res.mjs +25 -14
  81. package/src/db/EntityFilter.res +0 -4
  82. package/src/db/EntityFilter.res.mjs +1 -8
  83. package/src/db/EntityHistory.res +1 -1
  84. package/src/db/IndexCatalog.res +248 -0
  85. package/src/db/IndexCatalog.res.mjs +222 -0
  86. package/src/db/IndexDefinition.res +131 -0
  87. package/src/db/IndexDefinition.res.mjs +138 -0
  88. package/src/db/IndexManager.res +145 -0
  89. package/src/db/IndexManager.res.mjs +130 -0
  90. package/src/db/InternalTable.res +54 -23
  91. package/src/db/InternalTable.res.mjs +38 -23
  92. package/src/db/Table.res +24 -14
  93. package/src/db/Table.res.mjs +21 -16
  94. package/src/sources/AddressSet.res +5 -4
  95. package/src/sources/AddressStore.res +25 -14
  96. package/src/sources/AddressStore.res.mjs +10 -12
  97. package/src/sources/Evm.res +2 -2
  98. package/src/sources/Evm.res.mjs +1 -1
  99. package/src/sources/EvmChain.res +3 -3
  100. package/src/sources/EvmChain.res.mjs +3 -3
  101. package/src/sources/EvmHyperSyncSource.res +7 -7
  102. package/src/sources/EvmHyperSyncSource.res.mjs +6 -6
  103. package/src/sources/Fuel.res +2 -2
  104. package/src/sources/Fuel.res.mjs +1 -1
  105. package/src/sources/FuelHyperSyncClient.res +3 -0
  106. package/src/sources/FuelHyperSyncClient.res.mjs +1 -0
  107. package/src/sources/FuelHyperSyncSource.res +5 -7
  108. package/src/sources/FuelHyperSyncSource.res.mjs +5 -5
  109. package/src/sources/HyperSync.resi +2 -2
  110. package/src/sources/HyperSyncClient.res +5 -0
  111. package/src/sources/HyperSyncClient.res.mjs +1 -0
  112. package/src/sources/RpcSource.res +13 -14
  113. package/src/sources/RpcSource.res.mjs +13 -12
  114. package/src/sources/SimulateSource.res +42 -15
  115. package/src/sources/SimulateSource.res.mjs +27 -9
  116. package/src/sources/Source.res +1 -5
  117. package/src/sources/SourceManager.res +12 -12
  118. package/src/sources/SourceManager.res.mjs +11 -10
  119. package/src/sources/SourceManager.resi +2 -2
  120. package/src/sources/Svm.res +4 -5
  121. package/src/sources/Svm.res.mjs +5 -4
  122. package/src/sources/SvmHyperSyncClient.res +3 -0
  123. package/src/sources/SvmHyperSyncClient.res.mjs +1 -0
  124. package/src/sources/SvmHyperSyncSource.res +4 -4
  125. package/src/sources/SvmHyperSyncSource.res.mjs +3 -3
  126. package/src/tui/Tui.res +1 -1
  127. package/src/tui/Tui.res.mjs +2 -1
  128. package/src/tui/components/CustomHooks.res +5 -4
  129. 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
+ }