Skip to content

Commit f27a764

Browse files
committed
Issue 62 PR 03: Core generator indexes
1 parent 60c961b commit f27a764

1 file changed

Lines changed: 251 additions & 1 deletion

File tree

packages/generator/src/solidity/generateAppSolidity.ts

Lines changed: 251 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Access, Collection, FieldType, Relation, ThsField, ThsSchema, UniqueIndex } from '@tokenhost/schema';
1+
import type { Access, Collection, FieldType, QueryIndex, Relation, ThsField, ThsSchema, UniqueIndex } from '@tokenhost/schema';
22
import { computeSchemaHash } from '@tokenhost/schema';
33

44
type SolidityType = 'string' | 'uint256' | 'int256' | 'bool' | 'address' | 'bytes32';
@@ -49,6 +49,16 @@ function uniqueScope(index: UniqueIndex): 'active' | 'allTime' {
4949
return index.scope ?? 'active';
5050
}
5151

52+
function queryIndexMode(index: QueryIndex): 'equality' | 'tokenized' {
53+
return index.mode === 'tokenized' ? 'tokenized' : 'equality';
54+
}
55+
56+
function queryIndexKeyExpr(fieldType: FieldType, expr: string): string {
57+
return solidityStorageType(fieldType) === 'string'
58+
? `keccak256(bytes(${expr}))`
59+
: `keccak256(abi.encode(${expr}))`;
60+
}
61+
5262
class W {
5363
private lines: string[] = [];
5464
private indent = 0;
@@ -140,6 +150,8 @@ export function generateAppSolidity(schema: ThsSchema): { path: string; contents
140150
w.line('uint256 public constant MAX_LIST_LIMIT = 50;');
141151
w.line('uint256 public constant MAX_SCAN_STEPS = 1000;');
142152
w.line('uint256 public constant MAX_MULTICALL_CALLS = 20;');
153+
w.line('uint256 public constant MAX_TOKENIZED_INDEX_TOKENS = 8;');
154+
w.line('uint256 public constant MAX_TOKENIZED_INDEX_TOKEN_LENGTH = 32;');
143155
w.line();
144156

145157
// Errors
@@ -152,6 +164,8 @@ export function generateAppSolidity(schema: ThsSchema): { path: string; contents
152164
w.line('error TransferDisabled();');
153165
w.line('error InvalidRecipient();');
154166
w.line('error VersionMismatch(uint256 expected, uint256 got);');
167+
w.line('error TooManyIndexTokens();');
168+
w.line('error IndexTokenTooLong();');
155169
w.line();
156170

157171
// Events (SPEC 7.9)
@@ -228,6 +242,61 @@ export function generateAppSolidity(schema: ThsSchema): { path: string; contents
228242
});
229243
w.line();
230244

245+
w.block('function _isHashtagChar(bytes1 ch) internal pure returns (bool)', () => {
246+
w.line('return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A) || ch == 0x5F;');
247+
});
248+
w.line();
249+
250+
w.block('function _toLowerAscii(bytes1 ch) internal pure returns (bytes1)', () => {
251+
w.line('if (ch >= 0x41 && ch <= 0x5A) {');
252+
w.line(' return bytes1(uint8(ch) + 32);');
253+
w.line('}');
254+
w.line('return ch;');
255+
});
256+
w.line();
257+
258+
w.block('function _extractHashtagKeys(string memory value) internal pure returns (bytes32[] memory keys, uint256 count)', () => {
259+
w.line('bytes memory raw = bytes(value);');
260+
w.line('keys = new bytes32[](MAX_TOKENIZED_INDEX_TOKENS);');
261+
w.line('uint256 i = 0;');
262+
w.block('while (i < raw.length)', () => {
263+
w.line('if (raw[i] != 0x23) {');
264+
w.line(' i += 1;');
265+
w.line(' continue;');
266+
w.line('}');
267+
w.line('uint256 start = i + 1;');
268+
w.line('uint256 end = start;');
269+
w.block('while (end < raw.length && _isHashtagChar(raw[end]))', () => {
270+
w.line('end += 1;');
271+
});
272+
w.line('uint256 tokenLength = end - start;');
273+
w.line('i = end;');
274+
w.line('if (tokenLength == 0) {');
275+
w.line(' continue;');
276+
w.line('}');
277+
w.line('if (tokenLength > MAX_TOKENIZED_INDEX_TOKEN_LENGTH) revert IndexTokenTooLong();');
278+
w.line('bytes memory token = new bytes(tokenLength);');
279+
w.block('for (uint256 j = 0; j < tokenLength; j++)', () => {
280+
w.line('token[j] = _toLowerAscii(raw[start + j]);');
281+
});
282+
w.line('bytes32 key = keccak256(token);');
283+
w.line('bool seen = false;');
284+
w.block('for (uint256 j = 0; j < count; j++)', () => {
285+
w.line('if (keys[j] == key) {');
286+
w.line(' seen = true;');
287+
w.line(' break;');
288+
w.line('}');
289+
});
290+
w.line('if (seen) {');
291+
w.line(' continue;');
292+
w.line('}');
293+
w.line('if (count >= MAX_TOKENIZED_INDEX_TOKENS) revert TooManyIndexTokens();');
294+
w.line('keys[count] = key;');
295+
w.line('count += 1;');
296+
});
297+
});
298+
w.line();
299+
231300
const relationIndexes = buildRelationIndexes(schema);
232301

233302
// ---- Per-collection storage + methods ----
@@ -312,6 +381,13 @@ export function generateAppSolidity(schema: ThsSchema): { path: string; contents
312381
}
313382
if (c.indexes.unique.length > 0) w.line();
314383

384+
if (onChainIndexing) {
385+
for (const idx of c.indexes.index) {
386+
w.line(`mapping(bytes32 => uint256[]) private index_${C}_${idx.field};`);
387+
}
388+
if (c.indexes.index.length > 0) w.line();
389+
}
390+
315391
// Reverse reference indexes (append-only)
316392
if (onChainIndexing) {
317393
const rels = relationIndexes.filter((r) => r.from.name === C && r.rel.reverseIndex);
@@ -408,6 +484,143 @@ export function generateAppSolidity(schema: ThsSchema): { path: string; contents
408484
w.line();
409485
}
410486
}
487+
if (onChainIndexing) {
488+
for (const idx of c.indexes.index) {
489+
w.line(`mapping(bytes32 => uint256[]) private index_${C}_${idx.field};`);
490+
}
491+
if (c.indexes.index.length > 0) w.line();
492+
}
493+
494+
// Reverse reference indexes (append-only)
495+
if (onChainIndexing) {
496+
const rels = relationIndexes.filter((r) => r.from.name === C && r.rel.reverseIndex);
497+
for (const r of rels) {
498+
w.line(`mapping(uint256 => uint256[]) private refIndex_${C}_${r.rel.field};`);
499+
}
500+
if (rels.length > 0) w.line();
501+
}
502+
503+
// exists / getCount
504+
w.block(`function exists${C}(uint256 id) public view returns (bool)`, () => {
505+
w.line(`${record} storage r = ${cVar}Records[id];`);
506+
w.line('if (r.createdBy == address(0)) return false;');
507+
w.line('if (r.isDeleted) return false;');
508+
w.line('return true;');
509+
});
510+
w.line();
511+
512+
w.block(`function getCount${C}(bool includeDeleted) external view returns (uint256)`, () => {
513+
w.line('if (includeDeleted) {');
514+
w.line(` return nextId${C} - 1;`);
515+
w.line('}');
516+
w.line(`return activeCount${C};`);
517+
});
518+
w.line();
519+
520+
// get
521+
w.block(`function get${C}(uint256 id, bool includeDeleted) public view returns (${record} memory)`, () => {
522+
w.line(`${record} storage r = ${cVar}Records[id];`);
523+
w.line('if (r.createdBy == address(0)) revert RecordNotFound();');
524+
w.line('if (!includeDeleted && r.isDeleted) revert RecordIsDeleted();');
525+
w.line('return r;');
526+
});
527+
w.line();
528+
529+
w.block(`function get${C}(uint256 id) external view returns (${record} memory)`, () => {
530+
w.line(`return get${C}(id, false);`);
531+
});
532+
w.line();
533+
534+
// listIds
535+
w.block(
536+
`function listIds${C}(uint256 cursorIdExclusive, uint256 limit, bool includeDeleted) external view returns (uint256[] memory)`,
537+
() => {
538+
w.line('if (limit > MAX_LIST_LIMIT) revert InvalidLimit();');
539+
w.line(`uint256 cursor = cursorIdExclusive;`);
540+
w.line(`uint256 nextId = nextId${C};`);
541+
w.line('if (cursor == 0 || cursor > nextId) {');
542+
w.line(' cursor = nextId;');
543+
w.line('}');
544+
w.line('uint256[] memory tmp = new uint256[](limit);');
545+
w.line('uint256 found = 0;');
546+
w.line('uint256 steps = 0;');
547+
w.line('uint256 id = cursor;');
548+
w.block('while (id > 1 && found < limit && steps < MAX_SCAN_STEPS)', () => {
549+
w.line('id--;');
550+
w.line('steps++;');
551+
w.line(`${record} storage r = ${cVar}Records[id];`);
552+
w.line('if (r.createdBy == address(0)) { continue; }');
553+
w.line('if (!includeDeleted && r.isDeleted) { continue; }');
554+
w.line('tmp[found] = id;');
555+
w.line('found++;');
556+
});
557+
w.line('uint256[] memory out = new uint256[](found);');
558+
w.block('for (uint256 i = 0; i < found; i++)', () => {
559+
w.line('out[i] = tmp[i];');
560+
});
561+
w.line('return out;');
562+
}
563+
);
564+
w.line();
565+
566+
// Reverse reference accessor(s)
567+
if (onChainIndexing && c.relations) {
568+
for (const rel of c.relations.filter((r) => r.reverseIndex)) {
569+
w.block(
570+
`function listByRef${C}_${rel.field}(uint256 refId, uint256 offset, uint256 limit) external view returns (uint256[] memory)`,
571+
() => {
572+
w.line('if (limit > MAX_LIST_LIMIT) revert InvalidLimit();');
573+
w.line(`uint256[] storage bucket = refIndex_${C}_${rel.field}[refId];`);
574+
w.line('if (offset >= bucket.length) {');
575+
w.line(' return new uint256[](0);');
576+
w.line('}');
577+
w.line('uint256 end = offset + limit;');
578+
w.line('if (end > bucket.length) end = bucket.length;');
579+
w.line('uint256 outLen = end - offset;');
580+
w.line('uint256[] memory out = new uint256[](outLen);');
581+
w.block('for (uint256 i = 0; i < outLen; i++)', () => {
582+
w.line('out[i] = bucket[offset + i];');
583+
});
584+
w.line('return out;');
585+
}
586+
);
587+
w.line();
588+
}
589+
}
590+
591+
if (onChainIndexing) {
592+
for (const idx of c.indexes.index) {
593+
w.block(
594+
`function listByIndex${C}_${idx.field}(bytes32 key, uint256 offset, uint256 limit) external view returns (uint256[] memory)`,
595+
() => {
596+
w.line('if (limit > MAX_LIST_LIMIT) revert InvalidLimit();');
597+
w.line(`uint256[] storage bucket = index_${C}_${idx.field}[key];`);
598+
w.line('if (offset >= bucket.length) {');
599+
w.line(' return new uint256[](0);');
600+
w.line('}');
601+
w.line('uint256 end = offset + limit;');
602+
w.line('if (end > bucket.length) end = bucket.length;');
603+
w.line('uint256 outLen = end - offset;');
604+
w.line('uint256[] memory out = new uint256[](outLen);');
605+
w.block('for (uint256 i = 0; i < outLen; i++)', () => {
606+
w.line('out[i] = bucket[offset + i];');
607+
});
608+
w.line('return out;');
609+
}
610+
);
611+
w.line();
612+
613+
if (queryIndexMode(idx) === 'tokenized') {
614+
w.block(`function _indexHashtag${C}_${idx.field}(string memory value, uint256 id) internal`, () => {
615+
w.line('(bytes32[] memory keys, uint256 count) = _extractHashtagKeys(value);');
616+
w.block('for (uint256 i = 0; i < count; i++)', () => {
617+
w.line(`index_${C}_${idx.field}[keys[i]].push(id);`);
618+
});
619+
});
620+
w.line();
621+
}
622+
}
623+
}
411624

412625
// create
413626
const createFnName = `create${C}`;
@@ -468,6 +681,18 @@ export function generateAppSolidity(schema: ThsSchema): { path: string; contents
468681
w.line(`unique_${C}_${u.field}[key_${u.field}] = id;`);
469682
}
470683

684+
if (onChainIndexing) {
685+
for (const idx of c.indexes.index) {
686+
const f = c.fields.find((x) => x.name === idx.field);
687+
if (!f) continue;
688+
if (queryIndexMode(idx) === 'tokenized') {
689+
w.line(`_indexHashtag${C}_${idx.field}(input.${idx.field}, id);`);
690+
} else {
691+
w.line(`index_${C}_${idx.field}[${queryIndexKeyExpr(f.type, `input.${idx.field}`)}].push(id);`);
692+
}
693+
}
694+
}
695+
471696
// reverse index maintenance
472697
if (onChainIndexing && c.relations) {
473698
for (const rel of c.relations.filter((r) => r.reverseIndex)) {
@@ -525,6 +750,31 @@ export function generateAppSolidity(schema: ThsSchema): { path: string; contents
525750
w.line('}');
526751
}
527752

753+
if (onChainIndexing) {
754+
const queryByField = new Map(c.indexes.index.map((idx) => [idx.field, idx]));
755+
for (const field of c.updateRules.mutable) {
756+
const idx = queryByField.get(field);
757+
if (!idx) continue;
758+
const f = c.fields.find((x) => x.name === field);
759+
if (!f) continue;
760+
if (queryIndexMode(idx) === 'tokenized') {
761+
w.line(`bytes32 oldFieldHash_${field} = keccak256(bytes(r.${field}));`);
762+
w.line(`bytes32 newFieldHash_${field} = keccak256(bytes(${field}));`);
763+
w.line(`if (oldFieldHash_${field} != newFieldHash_${field}) {`);
764+
w.line(` _indexHashtag${C}_${field}(${field}, id);`);
765+
w.line('}');
766+
} else {
767+
const oldKey = queryIndexKeyExpr(f.type, `r.${field}`);
768+
const newKey = queryIndexKeyExpr(f.type, field);
769+
w.line(`bytes32 oldIndexKey_${field} = ${oldKey};`);
770+
w.line(`bytes32 newIndexKey_${field} = ${newKey};`);
771+
w.line(`if (oldIndexKey_${field} != newIndexKey_${field}) {`);
772+
w.line(` index_${C}_${field}[newIndexKey_${field}].push(id);`);
773+
w.line('}');
774+
}
775+
}
776+
}
777+
528778
for (const field of c.updateRules.mutable) {
529779
w.line(`r.${field} = ${field};`);
530780
}

0 commit comments

Comments
 (0)