Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Extracts the single-quoted string literals from a live CHECK expression
* reprint, in order of appearance, with doubled quotes unescaped
* (`'O''Brien'` → `O'Brien`). A text scan only — no predicate shape is
* recognized; casts, operators, and identifiers are skipped. An expression
* with no literals yields an empty list.
*/
export function harvestCheckLiterals(expression: string): string[] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incomprehensible. "Harvest" is not our ubiquitous language

const literals: string[] = [];
let index = 0;
while (index < expression.length) {
if (expression[index] !== `'`) {
index += 1;
continue;
}
index += 1;
let value = '';
while (index < expression.length) {
if (expression[index] !== `'`) {
value += expression[index];
index += 1;
continue;
}
if (expression[index + 1] === `'`) {
value += `'`;
index += 2;
continue;
}
index += 1;
literals.push(value);
break;
}
}
return literals;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { toEnumMemberName, toEnumName } from '@internal/family-sql/psl-infer';
import { collectScalarTypeConstructors } from '@internal/framework-components/authoring';
import type {
PslExtensionBlock,
PslExtensionBlockParamValue,
} from '@internal/framework-components/psl-ast';
import { postgresAuthoringTypes } from '../authoring';
import {
buildTopLevelNameMap,
createUniqueFieldName,
type TopLevelNameResult,
} from './infer-names';
import { POSTGRES_PSL_TYPE_NAMES } from './postgres-type-map';
import { escapePslString, SYNTHETIC_SPAN } from './psl-literals';

export const PSL_SCALAR_TYPE_NAMES = new Set([
const FRAMEWORK_SCALAR_TYPE_NAMES = [
'String',
'Boolean',
'Int',
Expand All @@ -20,6 +23,20 @@ export const PSL_SCALAR_TYPE_NAMES = new Set([
'DateTime',
'Json',
'Bytes',
] as const;

/**
* Every name that resolves as a type in column position of inferred output:
* the framework scalars, every PSL name the Postgres type map can emit
* (`Uuid`, `VarChar`, `Timestamptz`, …), and the target pack's own zero-arg
* type constructors (`BigIntNumber`, `UnboundedInt`). An inferred enum block
* claiming one of these names would silently retype every column of that
* type, so enum naming treats the whole set as reserved.
*/
export const PSL_SCALAR_TYPE_NAMES: ReadonlySet<string> = new Set([
...FRAMEWORK_SCALAR_TYPE_NAMES,
...POSTGRES_PSL_TYPE_NAMES,
...collectScalarTypeConstructors(postgresAuthoringTypes).keys(),
]);

type NativeEnumBlockResult = {
Expand Down Expand Up @@ -66,6 +83,48 @@ export function buildNativeEnumBlocks(
return { enumNameMap, enumBlocks };
}

/**
* Builds the family `enum` extension-block AST node for a recovered domain
* enum. Members print as `<sanitizedName> = "<value>"` pairs (names
* deduplicated within the block, values JSON-encoded verbatim) and the block
* carries `@@type("<codecId>")`. The caller owns name allocation — recovered
* names uniquify against the whole top-level scope before this is called.
*/
export function buildRecoveredEnumBlock(
name: string,
memberValues: readonly string[],
codecId: string,
): PslExtensionBlock {
const usedMemberNames = new Set<string>();
const parameters: Record<string, PslExtensionBlockParamValue> = {};
for (const value of memberValues) {
const memberName = createUniqueFieldName(toEnumMemberName(value), usedMemberNames);
usedMemberNames.add(memberName);
parameters[memberName] = { kind: 'value', raw: JSON.stringify(value), span: SYNTHETIC_SPAN };
}

return {
kind: 'enum',
keyword: 'enum',
name,
parameters,
blockAttributes: [
{
name: 'type',
args: [
{
kind: 'positional',
value: `"${escapePslString(codecId)}"`,
span: SYNTHETIC_SPAN,
},
],
span: SYNTHETIC_SPAN,
},
],
span: SYNTHETIC_SPAN,
};
}

function buildNativeEnumBlock(
name: string,
typeName: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ import {
SYNTHETIC_SPAN,
} from './psl-literals';

/**
* A recovered domain enum, keyed by column name per table: the allocated
* top-level PSL block name the column's field is typed by, and the proven
* member values `computeDerivedCheckNames` re-renders the membership check
* from.
*/
export interface RecoveredEnumField {
readonly pslName: string;
readonly memberValues: readonly string[];
}

const NO_RECOVERED_ENUMS: ReadonlyMap<string, RecoveredEnumField> = new Map();

export function buildModel(
table: SqlTableIR,
typeMap: PslTypeMap,
Expand All @@ -56,6 +69,7 @@ export function buildModel(
danglingForeignKeys: readonly DanglingForeignKeyInfo[],
rlsEnabled = false,
policySkipNotes: readonly string[] = [],
recoveredEnums: ReadonlyMap<string, RecoveredEnumField> = NO_RECOVERED_ENUMS,
): PslModel {
const { name: modelName, map: mapName } = toModelName(table.name);
const fieldNameMap = fieldNamesByTable.get(table.name);
Expand All @@ -75,7 +89,7 @@ export function buildModel(
}
}

const derivedCheckNames = computeDerivedCheckNames(table);
const derivedCheckNames = computeDerivedCheckNames(table, recoveredEnums);

const fields: PslField[] = [];
for (const column of Object.values(table.columns)) {
Expand All @@ -93,6 +107,7 @@ export function buildModel(
singlePkConstraintName,
uniqueColumns,
derivedCheckNames,
recoveredEnums.get(column.name),
),
);
}
Expand Down Expand Up @@ -184,20 +199,25 @@ export function buildModel(
* the `@noCheck` waiver below and `@@check` exclusion in `buildModel`, so the
* two decisions cannot drift apart.
*
* `membership` is unreachable here today: infer never emits domain enums
* (`enumType()` is not inferred), so no inferred column has member values and
* no membership check is ever derived. The day domain-enum inference exists,
* its slice extends this by threading the column's member values through.
* A `membership` check is derived only for a column Path A recovery proved:
* `recoveredEnums` carries its member values, so the re-render below produces
* the exact authored predicate and the live check's wire name lands in the
* derived set — which is what suppresses its `@@check` and its `@noCheck`
* waiver with no recovery-specific exclusion code. A column with no
* recovered enum renders no membership candidate at all.
*/
function computeDerivedCheckNames(table: SqlTableIR): ReadonlySet<string> {
function computeDerivedCheckNames(
table: SqlTableIR,
recoveredEnums: ReadonlyMap<string, RecoveredEnumField>,
): ReadonlySet<string> {
const liveCheckNames = new Set((table.checks ?? []).map((check) => check.name));
const derivedCheckNames = new Set<string>();
for (const column of Object.values(table.columns)) {
for (const candidate of postgresRenderCheckExpressions({
tableName: table.name,
columnName: column.name,
many: column.many === true,
memberValues: undefined,
memberValues: recoveredEnums.get(column.name)?.memberValues,
})) {
const derivedName = formatWireName(
composeCheckWirePrefix(table.name, column.name, candidate.kind),
Expand All @@ -224,6 +244,7 @@ function buildScalarField(
singlePkConstraintName: string | undefined,
uniqueColumns: ReadonlyMap<string, string | undefined>,
derivedCheckNames: ReadonlySet<string>,
recoveredEnum: RecoveredEnumField | undefined,
): PslField {
const resolvedField = fieldNameMap?.get(column.name);
const fieldName = resolvedField?.fieldName ?? toFieldName(column.name).name;
Expand Down Expand Up @@ -271,6 +292,14 @@ function buildScalarField(
};
}

// A recovered domain-enum column takes the authored form: the enum's bare
// top-level name, no type constructor — the storage type comes from the
// enum block's `@@type`, not from the scalar resolution above.
if (recoveredEnum !== undefined) {
typeName = recoveredEnum.pslName;
typeConstructor = undefined;
}

const attributes: PslFieldAttribute[] = [];
const isId = isSinglePk && pkColumns.has(column.name);
if (isId) {
Expand Down Expand Up @@ -329,7 +358,7 @@ function buildScalarField(
tableName: table.name,
columnName: column.name,
many: true,
memberValues: undefined,
memberValues: recoveredEnum?.memberValues,
})
.filter(
(candidate) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { SqlDescribedContractSpace } from '@internal/family-sql/control';
import type { EnumInfo, PslPrinterOptions } from '@internal/family-sql/psl-infer';
import { inferRelations, parseRawDefault, toModelName } from '@internal/family-sql/psl-infer';
import {
inferRelations,
parseRawDefault,
toEnumName,
toModelName,
} from '@internal/family-sql/psl-infer';
import { coordinateKey } from '@internal/framework-components/ir';
import type {
PslDocumentAst,
Expand All @@ -17,18 +22,29 @@ import { SqlSchemaIR, SqlTableIR } from '@internal/sql-schema-ir/types';
import { postgresError } from '../errors';
import type { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
import type { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
import { buildNativeEnumBlocks, PSL_SCALAR_TYPE_NAMES } from './infer-enum-blocks';
import {
buildNativeEnumBlocks,
buildRecoveredEnumBlock,
PSL_SCALAR_TYPE_NAMES,
} from './infer-enum-blocks';
import {
describedContractOwners,
type ForeignKeyResolution,
resolveForeignKeys,
} from './infer-foreign-keys';
import type { RecoveredEnumField } from './infer-model-blocks';
import { buildModel } from './infer-model-blocks';
import { buildFieldNamesByTable, buildTopLevelNameMap, topologicalSort } from './infer-names';
import {
buildFieldNamesByTable,
buildTopLevelNameMap,
createUniqueFieldName,
topologicalSort,
} from './infer-names';
import { buildPolicyBlocks } from './infer-policy-blocks';
import { createPostgresDefaultMapping } from './postgres-default-mapping';
import { createPostgresTypeMap } from './postgres-type-map';
import { SYNTHETIC_SPAN } from './psl-literals';
import { recoverDomainEnumColumns } from './recover-domain-enums';

/**
* Infers a PSL AST (for `printPsl`) from an introspected Postgres schema tree.
Expand Down Expand Up @@ -308,25 +324,15 @@ export function buildPslDocumentAst(
]);
const { relationsByTable } = inferRelations(schemaIR.tables, modelNameMap);

const policyEmission = buildPolicyBlocks(
rlsExtras?.policiesByTable ?? new Map(),
modelNameMap,
new Set([
...modelNameMap.values(),
...bareEnumNameMap.values(),
...topLevelExtensionBlocks.map((block) => block.name),
]),
);

// A caller's block shares one top-level name scope with the models, the
// native enums, PSL's scalar type names, and the caller's other blocks. A
// clash costs differently depending on the partner: against a scalar name
// the enum wins the type lookup and retypes every field of that type
// (`psl-column-resolution` consults enums before scalars), while against a
// model it is two declarations claiming one top-level name, which does not
// parse back. Either way the block cannot be renamed here, so it is refused.
// Policies are absent from this set on purpose — they were handed these
// names above and have already renamed themselves out of the way.
// Policies are absent from this set on purpose — they are handed these
// names below and rename themselves out of the way.
const claimedTopLevelNames = new Set([
...PSL_SCALAR_TYPE_NAMES,
...modelNameMap.values(),
Expand All @@ -345,6 +351,42 @@ export function buildPslDocumentAst(
claimedTopLevelNames.add(block.name);
}

// Path A domain-enum recovery. Names are allocated against the fully
// claimed top-level scope with the numeric-suffix disambiguator, so a
// recovered block can never trip the collision throw above (which stays
// for external callers of `topLevelExtensionBlocks`).
const recoveredColumnsByTable = recoverDomainEnumColumns(schemaIR.tables);
const recoveredEnumBlocks: PslExtensionBlock[] = [];
const recoveredEnumsByTable = new Map<string, ReadonlyMap<string, RecoveredEnumField>>();
for (const table of Object.values(schemaIR.tables)) {
const recoveredColumns = recoveredColumnsByTable.get(table.name);
if (recoveredColumns === undefined) continue;
const byColumn = new Map<string, RecoveredEnumField>();
for (const column of Object.values(table.columns)) {
const entry = recoveredColumns.get(column.name);
if (entry === undefined) continue;
const pslName = createUniqueFieldName(
toEnumName(`${table.name}_${column.name}`).name,
claimedTopLevelNames,
);
claimedTopLevelNames.add(pslName);
recoveredEnumBlocks.push(buildRecoveredEnumBlock(pslName, entry.memberValues, entry.codecId));
byColumn.set(column.name, { pslName, memberValues: entry.memberValues });
}
recoveredEnumsByTable.set(table.name, byColumn);
}
const allTopLevelBlocks = [...topLevelExtensionBlocks, ...recoveredEnumBlocks];

const policyEmission = buildPolicyBlocks(
rlsExtras?.policiesByTable ?? new Map(),
modelNameMap,
new Set([
...modelNameMap.values(),
...bareEnumNameMap.values(),
...allTopLevelBlocks.map((block) => block.name),
]),
);

const models: PslModel[] = [];
for (const table of Object.values(schemaIR.tables)) {
models.push(
Expand All @@ -362,6 +404,7 @@ export function buildPslDocumentAst(
danglingForeignKeysByTable.get(table.name) ?? [],
rlsExtras?.rlsEnabledTables.has(table.name) ?? false,
policyEmission.skipNotesByTable.get(table.name) ?? [],
recoveredEnumsByTable.get(table.name),
),
);
}
Expand Down Expand Up @@ -394,15 +437,15 @@ export function buildPslDocumentAst(
const separateTopLevelBucket =
namespaceName !== undefined &&
namespaceName !== UNSPECIFIED_PSL_NAMESPACE_ID &&
topLevelExtensionBlocks.length > 0;
allTopLevelBlocks.length > 0;

const namespaces: PslNamespace[] = [];
if (separateTopLevelBucket) {
namespaces.push(
makePslNamespace({
kind: 'namespace',
name: UNSPECIFIED_PSL_NAMESPACE_ID,
entries: makePslNamespaceEntries([], [], topLevelExtensionBlocks),
entries: makePslNamespaceEntries([], [], allTopLevelBlocks),
span: SYNTHETIC_SPAN,
}),
);
Expand All @@ -415,7 +458,7 @@ export function buildPslDocumentAst(
sortedModels,
[],
[
...(separateTopLevelBucket ? [] : topLevelExtensionBlocks),
...(separateTopLevelBucket ? [] : allTopLevelBlocks),
...enumBlocks,
...policyEmission.blocks,
],
Expand Down
Loading
Loading