@secondlayer/subgraphs 3.19.10 → 3.20.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.
@@ -411,7 +411,16 @@ type AggregateResult<
411
411
  max: Record<ColsOf<A, "max">, string | null>
412
412
  } : unknown);
413
413
  interface SubgraphTableClient<TRow> {
414
- findMany(options?: FindManyOptions<TRow>): Promise<TRow[]>;
414
+ /**
415
+ * List rows. `fields` narrows BOTH the wire payload and the return type:
416
+ * the server projects the SELECT to exactly those columns, so an
417
+ * unrequested field is physically absent — reading it is a compile error,
418
+ * not `undefined`. The `const` type parameter keeps the literal field
419
+ * names without `as const` at the call site (same pattern as `aggregate`).
420
+ */
421
+ findMany<const F extends readonly (keyof TRow & string)[] = readonly (keyof TRow & string)[]>(options?: Omit<FindManyOptions<TRow>, "fields"> & {
422
+ fields?: F
423
+ }): Promise<Pick<TRow, F[number]>[]>;
415
424
  count(where?: WhereInput<TRow> & SystemWhereAliases): Promise<number>;
416
425
  /**
417
426
  * Scalar aggregates over the filtered set. SUM/MIN/MAX accept numeric columns
@@ -793,19 +802,41 @@ interface TableDiff {
793
802
  removedTables: string[];
794
803
  /** Per-table column diffs (only for tables present in both) */
795
804
  tables: Record<string, ColumnDiff>;
805
+ /** Per-table uniqueKeys / composite-index diffs (tables present in both) */
806
+ constraints: Record<string, ConstraintDiff>;
796
807
  }
797
808
  interface ColumnDiff {
798
809
  added: string[];
799
810
  removed: string[];
811
+ /** Data-shape changes (type/nullable/default) — breaking. */
800
812
  changed: string[];
813
+ /** Only the `indexed`/`search` flags differ — index DDL, never a rebuild. */
814
+ indexChanged: string[];
815
+ }
816
+ interface ConstraintDiff {
817
+ /** New uniqueKeys entries → `ALTER TABLE … ADD CONSTRAINT` (additive). */
818
+ addedUniqueKeys: string[][];
819
+ /** Removed uniqueKeys entries — breaking: handler upserts may target them. */
820
+ removedUniqueKeys: string[][];
821
+ /** New composite `indexes` entries → CREATE INDEX (additive). */
822
+ addedIndexes: string[][];
823
+ /** Removed composite `indexes` entries → DROP INDEX (additive). */
824
+ removedIndexes: string[][];
801
825
  }
802
826
  /**
803
827
  * Compare two multi-table subgraph schemas and return differences.
828
+ *
829
+ * Index-only changes (`indexed`/`search` flags, composite `indexes`) are
830
+ * reported separately from data-shape changes: an index never requires a
831
+ * rebuild, so it must never land in `changed`/breaking — flipping
832
+ * `indexed: true` on a populated column used to cost a full reindex.
804
833
  */
805
834
  declare function diffSchema(existing: SubgraphSchema, incoming: SubgraphSchema): TableDiff;
806
835
  /**
807
836
  * Returns true if the diff contains any breaking changes
808
- * (removed tables, removed columns, or changed column types).
837
+ * (removed tables, removed columns, changed column types, or removed
838
+ * uniqueKeys — handler upserts `ON CONFLICT` against those).
839
+ * Index-flag and composite-index changes are never breaking.
809
840
  */
810
841
  declare function hasBreakingChanges(diff: TableDiff): {
811
842
  breaking: boolean
@@ -816,6 +847,10 @@ interface DeployDiff {
816
847
  removedTables: string[];
817
848
  addedColumns: Record<string, string[]>;
818
849
  breakingChanges: string[];
850
+ /** Columns whose only change was the `indexed`/`search` flag (index DDL applied in place). */
851
+ indexChanges?: Record<string, string[]>;
852
+ /** uniqueKeys added in place via ALTER TABLE … ADD CONSTRAINT. */
853
+ addedUniqueKeys?: Record<string, string[][]>;
819
854
  }
820
855
  interface ByoMigrationPlan {
821
856
  schemaName: string;
@@ -877,6 +912,8 @@ declare function deploySchema(db: AnyDb, def: SubgraphDefinition, handlerPath: s
877
912
  subgraphId: string
878
913
  version: string
879
914
  diff?: DeployDiff
915
+ /** Non-fatal caveats the caller should surface (e.g. handler grafts at tip). */
916
+ warnings?: string[]
880
917
  }>;
881
918
  /** A (decoded event type, optional contract scope) pair the sparse probe
882
919
  * checks. Contract scoping is what makes token-subgraph reindexes leap over
package/dist/src/index.js CHANGED
@@ -3843,31 +3843,53 @@ import { sql as sql4 } from "kysely";
3843
3843
  function toJsonSafe(obj) {
3844
3844
  return JSON.parse(JSON.stringify(obj, (_key, value) => typeof value === "bigint" ? value.toString() : value));
3845
3845
  }
3846
+ function columnDataShape(col) {
3847
+ const { indexed, search, ...rest } = col ?? {};
3848
+ return JSON.stringify(rest, Object.keys(rest).sort());
3849
+ }
3850
+ function diffKeyLists(existing, incoming) {
3851
+ const key2 = (cols) => JSON.stringify(cols);
3852
+ const existingSet = new Set((existing ?? []).map(key2));
3853
+ const incomingSet = new Set((incoming ?? []).map(key2));
3854
+ return {
3855
+ added: (incoming ?? []).filter((cols) => !existingSet.has(key2(cols))),
3856
+ removed: (existing ?? []).filter((cols) => !incomingSet.has(key2(cols)))
3857
+ };
3858
+ }
3846
3859
  function diffSchema(existing, incoming) {
3847
3860
  const existingTables = new Set(Object.keys(existing));
3848
3861
  const incomingTables = new Set(Object.keys(incoming));
3849
3862
  const addedTables = [...incomingTables].filter((t) => !existingTables.has(t));
3850
3863
  const removedTables = [...existingTables].filter((t) => !incomingTables.has(t));
3851
3864
  const tables = {};
3865
+ const constraints = {};
3852
3866
  for (const tableName of incomingTables) {
3853
3867
  if (!existingTables.has(tableName))
3854
3868
  continue;
3855
- const existingCols = existing[tableName]?.columns;
3856
- const incomingCols = incoming[tableName]?.columns;
3869
+ const existingTable = existing[tableName];
3870
+ const incomingTable = incoming[tableName];
3871
+ const existingCols = existingTable?.columns;
3872
+ const incomingCols = incomingTable?.columns;
3857
3873
  const existingKeys = new Set(Object.keys(existingCols));
3858
3874
  const incomingKeys = new Set(Object.keys(incomingCols));
3875
+ const common = [...incomingKeys].filter((k) => existingKeys.has(k));
3876
+ const sortedStringify = (o) => JSON.stringify(o, Object.keys(o).sort());
3859
3877
  tables[tableName] = {
3860
3878
  added: [...incomingKeys].filter((k) => !existingKeys.has(k)),
3861
3879
  removed: [...existingKeys].filter((k) => !incomingKeys.has(k)),
3862
- changed: [...incomingKeys].filter((k) => {
3863
- if (!existingKeys.has(k))
3864
- return false;
3865
- const sortedStringify = (o) => JSON.stringify(o, Object.keys(o).sort());
3866
- return sortedStringify(existingCols[k]) !== sortedStringify(incomingCols[k]);
3867
- })
3880
+ changed: common.filter((k) => columnDataShape(existingCols[k]) !== columnDataShape(incomingCols[k])),
3881
+ indexChanged: common.filter((k) => columnDataShape(existingCols[k]) === columnDataShape(incomingCols[k]) && sortedStringify(existingCols[k]) !== sortedStringify(incomingCols[k]))
3882
+ };
3883
+ const uq = diffKeyLists(existingTable?.uniqueKeys, incomingTable?.uniqueKeys);
3884
+ const ix = diffKeyLists(existingTable?.indexes, incomingTable?.indexes);
3885
+ constraints[tableName] = {
3886
+ addedUniqueKeys: uq.added,
3887
+ removedUniqueKeys: uq.removed,
3888
+ addedIndexes: ix.added,
3889
+ removedIndexes: ix.removed
3868
3890
  };
3869
3891
  }
3870
- return { addedTables, removedTables, tables };
3892
+ return { addedTables, removedTables, tables, constraints };
3871
3893
  }
3872
3894
  function hasBreakingChanges(diff) {
3873
3895
  const reasons = [];
@@ -3882,6 +3904,11 @@ function hasBreakingChanges(diff) {
3882
3904
  reasons.push(`${table}: changed columns [${colDiff.changed.join(", ")}]`);
3883
3905
  }
3884
3906
  }
3907
+ for (const [table, conDiff] of Object.entries(diff.constraints ?? {})) {
3908
+ if (conDiff.removedUniqueKeys.length > 0) {
3909
+ reasons.push(`${table}: removed uniqueKeys [${conDiff.removedUniqueKeys.map((cols) => `(${cols.join(", ")})`).join(", ")}]`);
3910
+ }
3911
+ }
3885
3912
  return { breaking: reasons.length > 0, reasons };
3886
3913
  }
3887
3914
  function bumpPatch(version) {
@@ -3987,7 +4014,12 @@ async function deploySchema(db, def, handlerPath, opts) {
3987
4014
  return {
3988
4015
  action: handlerChanged ? "handler_updated" : "unchanged",
3989
4016
  subgraphId: existing.id,
3990
- version: existing.version
4017
+ version: existing.version,
4018
+ ...handlerChanged ? {
4019
+ warnings: [
4020
+ "handler changed but schema did not: new logic applies from the current tip only. Rows already indexed were computed by the previous handler. To recompute history, redeploy with --reindex (or backfill a range)."
4021
+ ]
4022
+ } : {}
3991
4023
  };
3992
4024
  }
3993
4025
  if (existing.schema_hash === hash && opts?.forceReindex) {
@@ -4058,6 +4090,65 @@ async function deploySchema(db, def, handlerPath, opts) {
4058
4090
  }
4059
4091
  }
4060
4092
  }
4093
+ const existingSchema = existing.definition.schema;
4094
+ const flagFlips = {};
4095
+ const needsTrgmForFlips = Object.entries(diff.tables).some(([tableName, colDiff]) => colDiff.indexChanged.some((colName) => def.schema[tableName]?.columns[colName]?.search && !existingSchema[tableName]?.columns[colName]?.search));
4096
+ if (needsTrgmForFlips) {
4097
+ await sql4.raw("CREATE EXTENSION IF NOT EXISTS pg_trgm").execute(ddlDb);
4098
+ }
4099
+ for (const [tableName, colDiff] of Object.entries(diff.tables)) {
4100
+ if (colDiff.indexChanged.length === 0)
4101
+ continue;
4102
+ flagFlips[tableName] = colDiff.indexChanged;
4103
+ const qualifiedName = `${schemaName}.${tableName}`;
4104
+ for (const colName of colDiff.indexChanged) {
4105
+ const was = existingSchema[tableName]?.columns[colName];
4106
+ const now = def.schema[tableName]?.columns[colName];
4107
+ if (!now)
4108
+ continue;
4109
+ const plainIdx = `idx_${schemaName}_${tableName}_${colName}`;
4110
+ const trgmIdx = `idx_${schemaName}_${tableName}_${colName}_trgm`;
4111
+ if (now.indexed && !was?.indexed) {
4112
+ await sql4.raw(`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${plainIdx} ON ${qualifiedName} (${colName})`).execute(ddlDb);
4113
+ } else if (!now.indexed && was?.indexed) {
4114
+ await sql4.raw(`DROP INDEX CONCURRENTLY IF EXISTS ${schemaName}.${plainIdx}`).execute(ddlDb);
4115
+ }
4116
+ if (now.search && !was?.search) {
4117
+ await sql4.raw(`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${trgmIdx} ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`).execute(ddlDb);
4118
+ } else if (!now.search && was?.search) {
4119
+ await sql4.raw(`DROP INDEX CONCURRENTLY IF EXISTS ${schemaName}.${trgmIdx}`).execute(ddlDb);
4120
+ }
4121
+ }
4122
+ }
4123
+ const addedUq = {};
4124
+ for (const [tableName, conDiff] of Object.entries(diff.constraints)) {
4125
+ const qualifiedName = `${schemaName}.${tableName}`;
4126
+ for (const cols of conDiff.addedUniqueKeys) {
4127
+ const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
4128
+ const exists = await sql4`
4129
+ SELECT EXISTS (
4130
+ SELECT 1 FROM pg_constraint
4131
+ WHERE conname = ${constraintName}
4132
+ ) AS "exists"
4133
+ `.execute(ddlDb).then((r) => r.rows[0]?.exists ?? false);
4134
+ if (!exists) {
4135
+ await sql4.raw(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`).execute(ddlDb);
4136
+ }
4137
+ addedUq[tableName] = [...addedUq[tableName] ?? [], cols];
4138
+ }
4139
+ for (const cols of conDiff.addedIndexes) {
4140
+ const i = (def.schema[tableName]?.indexes ?? []).findIndex((entry) => JSON.stringify(entry) === JSON.stringify(cols));
4141
+ const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
4142
+ await sql4.raw(`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`).execute(ddlDb);
4143
+ }
4144
+ for (const cols of conDiff.removedIndexes) {
4145
+ const i = (existingSchema[tableName]?.indexes ?? []).findIndex((entry) => JSON.stringify(entry) === JSON.stringify(cols));
4146
+ if (i < 0)
4147
+ continue;
4148
+ const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
4149
+ await sql4.raw(`DROP INDEX CONCURRENTLY IF EXISTS ${schemaName}.${idxName}`).execute(ddlDb);
4150
+ }
4151
+ }
4061
4152
  const sg2 = await registerSubgraph(db, regData);
4062
4153
  const addedCols = {};
4063
4154
  for (const [t, colDiff] of Object.entries(diff.tables)) {
@@ -4068,7 +4159,9 @@ async function deploySchema(db, def, handlerPath, opts) {
4068
4159
  addedTables: diff.addedTables,
4069
4160
  removedTables: [],
4070
4161
  addedColumns: addedCols,
4071
- breakingChanges: []
4162
+ breakingChanges: [],
4163
+ ...Object.keys(flagFlips).length > 0 ? { indexChanges: flagFlips } : {},
4164
+ ...Object.keys(addedUq).length > 0 ? { addedUniqueKeys: addedUq } : {}
4072
4165
  };
4073
4166
  return {
4074
4167
  action: "updated",
@@ -4126,5 +4219,5 @@ export {
4126
4219
  ByoBreakingChangeError
4127
4220
  };
4128
4221
 
4129
- //# debugId=9D76C8C8FCA229DB64756E2164756E21
4222
+ //# debugId=E040C325764492FC64756E2164756E21
4130
4223
  //# sourceMappingURL=index.js.map