@happyvertical/smrt-core 0.40.69 → 0.41.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/AGENTS.md +29 -4
- package/README.md +20 -1
- package/agents/change-feed.md +1 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +786 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -3
- package/dist/cascade.d.ts +120 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +430 -0
- package/dist/cascade.js.map +1 -0
- package/dist/change-feed.d.ts +34 -2
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +52 -11
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +87 -9
- package/dist/class.js.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +130 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +290 -57
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +80 -6
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +102 -12
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +19 -4
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +5 -0
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/embedded-write-queue.d.ts +46 -0
- package/dist/embedded-write-queue.d.ts.map +1 -0
- package/dist/embedded-write-queue.js +66 -0
- package/dist/embedded-write-queue.js.map +1 -0
- package/dist/embeddings/storage.d.ts +7 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +29 -12
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +31 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +27 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +4 -7
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/static-manifest.js +10 -10
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +19 -19
- package/dist/migrations/differ.d.ts +211 -9
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +613 -50
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +4 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +6 -3
- package/dist/migrations/minor-units.d.ts +162 -0
- package/dist/migrations/minor-units.d.ts.map +1 -0
- package/dist/migrations/minor-units.js +381 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +6 -3
- package/dist/object.d.ts +142 -10
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +196 -41
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +101 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +177 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +3 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/manifest-field-merge.d.ts +12 -0
- package/dist/registry/manifest-field-merge.d.ts.map +1 -1
- package/dist/registry/manifest-field-merge.js +14 -2
- package/dist/registry/manifest-field-merge.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +22 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +205 -165
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +35 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +41 -46
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +61 -83
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +45 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +92 -28
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +104 -0
- package/dist/schema/conflict-target.d.ts.map +1 -0
- package/dist/schema/conflict-target.js +129 -0
- package/dist/schema/conflict-target.js.map +1 -0
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +12 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +307 -6
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +510 -87
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index-utils.d.ts +120 -0
- package/dist/schema/index-utils.d.ts.map +1 -1
- package/dist/schema/index-utils.js +242 -1
- package/dist/schema/index-utils.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +602 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +65 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +187 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +2 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +5 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +20 -8
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/system/compatibility.d.ts +42 -0
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +182 -9
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +237 -0
- package/dist/system/retention.d.ts.map +1 -0
- package/dist/system/retention.js +497 -0
- package/dist/system/retention.js.map +1 -0
- package/dist/system/schema.d.ts +100 -15
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +81 -45
- package/dist/system/schema.js.map +1 -1
- package/dist/system/types.d.ts +0 -2
- package/dist/system/types.d.ts.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -9
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","names":[],"sources":["../../src/schema/generator.ts"],"sourcesContent":["/**\n * Schema generator for SMRT objects\n * Converts AST field definitions to database schema definitions\n */\n\nimport { createHash } from 'node:crypto';\nimport type {\n FieldDefinition,\n FieldMeta,\n ManifestColumnDefinition,\n ManifestIndexDefinition,\n ManifestSchema,\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types.js';\nimport { classnameToTablename } from '../utils/naming.js';\nimport { getDDLStrategy } from './ddl/index.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n formatDefaultValue as formatDefaultValueShared,\n quoteIdentifier,\n quoteStringLiteral,\n} from './sql-identifiers.js';\nimport type {\n ColumnDefinition,\n ForeignKeyDefinition,\n IndexDefinition,\n SchemaDefinition,\n SQLDataType,\n TriggerDefinition,\n} from './types.js';\n\n/**\n * Structural shape of a field as read from either the build-time manifest\n * (`FieldDefinition`) or the runtime ObjectRegistry (`getAllFields()` returns\n * `Map<string, any>` upstream). The schema generator consumes both through a\n * single set of property reads, so this interface captures every field key the\n * generator touches without coupling to either source's exact type.\n *\n * `_meta` carries the structured field-helper metadata (`FieldMeta`); a few\n * legacy fields (`indexed`, `idType`, `__tenancy`) may also appear at the top\n * level depending on the source, so they are declared here too.\n */\ninterface RegistryField {\n type?: FieldDefinition['type'];\n related?: string;\n required?: boolean;\n default?: unknown;\n description?: string;\n transient?: boolean;\n indexed?: boolean;\n idType?: 'uuid' | 'text';\n __tenancy?: FieldMeta['__tenancy'];\n _meta?: FieldMeta;\n}\n\ntype SchemaGeneratorConfig = {\n conflictColumns?: string[];\n idType?: 'uuid' | 'text';\n registry?: {\n getConfig?(className: string): { idType?: 'uuid' | 'text' };\n getDescendants(baseClassName: string): string[];\n getAllFields(className: string): Promise<Map<string, RegistryField>>;\n getSTIBase?(className: string): string | null;\n };\n};\n\nexport class SchemaGenerator {\n /**\n * Generate schema definition from SMRT object definition\n */\n generateSchema(objectDef: SmartObjectDefinition): SchemaDefinition {\n const tableName = this.getTableName(objectDef);\n const columns = this.generateColumns(\n objectDef.fields,\n objectDef.decoratorConfig,\n );\n const indexes = this.generateIndexes(objectDef, columns);\n const triggers = this.generateTriggers(objectDef, tableName);\n const foreignKeys = this.extractForeignKeys(columns);\n const dependencies = this.extractDependencies(objectDef, foreignKeys);\n const version = this.generateVersion(objectDef);\n\n return {\n tableName,\n columns,\n indexes,\n triggers,\n foreignKeys,\n dependencies,\n version,\n packageName: this.extractPackageName(objectDef.filePath),\n baseClass: objectDef.extends,\n };\n }\n\n /**\n * Convert field type to SQL data type\n */\n private mapFieldTypeToSQL(fieldType: FieldDefinition['type']): SQLDataType {\n switch (fieldType) {\n case 'text':\n return 'TEXT';\n case 'integer':\n return 'INTEGER';\n case 'decimal':\n return 'REAL';\n case 'boolean':\n return 'BOOLEAN';\n case 'datetime':\n return 'TIMESTAMP';\n case 'json':\n return 'JSON';\n case 'foreignKey':\n return 'UUID'; // Foreign keys default to UUID, then same-package refs can be reconciled to target idType\n case 'crossPackageRef':\n return 'UUID'; // Cross-package refs are UUID ids with no DDL FK constraint\n default:\n return 'TEXT'; // Default fallback\n }\n }\n\n private getIdColumnType(config?: { idType?: 'uuid' | 'text' }): SQLDataType {\n return config?.idType === 'text' ? 'TEXT' : 'UUID';\n }\n\n private getRelationshipColumnType(field: RegistryField): SQLDataType {\n if (field?._meta?.sqlType) {\n return String(field._meta.sqlType).toUpperCase() as SQLDataType;\n }\n\n if (\n field?.type === 'crossPackageRef' &&\n (field._meta?.idType === 'text' || field.idType === 'text')\n ) {\n return 'TEXT';\n }\n\n return this.mapFieldTypeToSQL(field?.type || 'text');\n }\n\n private getReferenceKind(\n field: RegistryField,\n ): ColumnDefinition['referenceKind'] | undefined {\n if (\n field?.__tenancy?.isTenantIdField ||\n field?._meta?.__tenancy?.isTenantIdField\n ) {\n return 'tenantId';\n }\n\n if (field?.type === 'foreignKey') {\n return 'foreignKey';\n }\n\n if (field?.type === 'crossPackageRef') {\n return 'crossPackageRef';\n }\n\n return undefined;\n }\n\n private shouldEmitDefault(\n field: RegistryField,\n defaultValue: unknown,\n ): boolean {\n return !(\n this.getReferenceKind(field) === 'tenantId' &&\n this.getRelationshipColumnType(field) === 'UUID' &&\n defaultValue === ''\n );\n }\n\n private getRegistryTargetIdColumnType(\n relatedName: string | undefined,\n registry: SchemaGeneratorConfig['registry'] | undefined,\n ): SQLDataType | undefined {\n if (!relatedName || !registry?.getConfig) {\n return undefined;\n }\n\n const targetName = relatedName.split('.')[0];\n const targetNames = [targetName];\n const stiBase = registry.getSTIBase?.(targetName);\n if (stiBase && !targetNames.includes(stiBase)) {\n targetNames.push(stiBase);\n }\n\n for (const name of targetNames) {\n const idType = registry.getConfig(name)?.idType;\n if (idType === 'text') {\n return 'TEXT';\n }\n if (idType === 'uuid') {\n return 'UUID';\n }\n }\n\n return undefined;\n }\n\n private reconcileRegistryForeignKeyColumnTypes(\n columns: Record<string, ColumnDefinition>,\n fields: Map<string, RegistryField>,\n registry: SchemaGeneratorConfig['registry'] | undefined,\n ): void {\n if (!registry?.getConfig) {\n return;\n }\n\n for (const [fieldName, field] of fields.entries()) {\n if (field.type !== 'foreignKey' || field._meta?.sqlType) {\n continue;\n }\n\n const targetIdType = this.getRegistryTargetIdColumnType(\n field.related,\n registry,\n );\n if (!targetIdType) {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n if (columns[columnName]) {\n columns[columnName] = {\n ...columns[columnName],\n type: targetIdType,\n };\n }\n }\n }\n\n /**\n * Generate column definitions\n */\n private generateColumns(\n fields: Record<string, FieldDefinition>,\n config?: { idType?: 'uuid' | 'text' },\n ): Record<string, ColumnDefinition> {\n const columns: Record<string, ColumnDefinition> = {};\n\n // Always include base SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n description: 'Primary identifier',\n };\n\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n\n // Add fields from object definition\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n // Skip id fields as we handle them above\n if (\n fieldName === 'id' ||\n fieldName === 'created_at' ||\n fieldName === 'updated_at'\n ) {\n continue;\n }\n\n // Skip transient fields (non-persisted)\n // NOTE: This filtering logic must match SmrtObject.toJSON()\n // Changes to field filtering should be applied in BOTH places\n if (fieldDef.transient || fieldDef._meta?.transient) {\n continue;\n }\n\n // Skip relationship fields that don't create columns\n // oneToMany and manyToMany are relationship metadata, not actual database columns\n // foreignKey DOES create a column (it stores the foreign key ID)\n // NOTE: This must match the filtering in SmrtObject.toJSON()\n if (fieldDef.type === 'oneToMany' || fieldDef.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column (STI only)\n // In CTI mode, meta fields shouldn't be used, but skip them anyway for safety\n if (fieldDef.type === 'meta') {\n continue;\n }\n\n const column: ColumnDefinition = {\n type: this.getRelationshipColumnType(fieldDef),\n referenceKind: this.getReferenceKind(fieldDef),\n // If _meta.nullable is true, the field can be null regardless of required\n // This handles field helpers like text({ required: true, nullable: true })\n notNull: fieldDef._meta?.nullable ? false : fieldDef.required || false,\n unique: fieldDef._meta?.unique || false,\n description: fieldDef.description,\n };\n\n // Handle default values\n if (\n fieldDef.default !== undefined &&\n this.shouldEmitDefault(fieldDef, fieldDef.default)\n ) {\n column.defaultValue = fieldDef.default;\n }\n // Note: Removed automatic NOT NULL DEFAULT '' for TEXT columns\n // This was forcing all TEXT fields to be NOT NULL regardless of required option\n // If DuckDB ANY type inference is an issue, it should be handled at the adapter level\n // or with an explicit field option, not by silently changing schema semantics\n\n // Handle foreign keys\n if (fieldDef.type === 'foreignKey' && fieldDef.related) {\n const [table, columnName = 'id'] = fieldDef.related.split('.');\n column.foreignKey = {\n table,\n column: columnName,\n onDelete: 'CASCADE', // Default behavior\n onUpdate: 'CASCADE',\n };\n }\n\n // Handle unique constraints\n if (fieldName === 'slug' || fieldName === 'email') {\n column.unique = true;\n }\n\n columns[fieldName] = column;\n }\n\n return columns;\n }\n\n /**\n * Generate index definitions\n */\n private generateIndexes(\n objectDef: SmartObjectDefinition,\n columns: Record<string, ColumnDefinition>,\n ): IndexDefinition[] {\n const indexes: IndexDefinition[] = [];\n const tableName = this.getTableName(objectDef);\n\n // Create indexes for foreign keys\n for (const [columnName, columnDef] of Object.entries(columns)) {\n if (columnDef.foreignKey) {\n indexes.push({\n name: `idx_${tableName}_${columnName}`,\n columns: [columnName],\n description: `Index for foreign key ${columnName}`,\n });\n }\n }\n\n // Create index for updated_at (common query pattern)\n indexes.push({\n name: `idx_${tableName}_updated_at`,\n columns: ['updated_at'],\n description: 'Index for timestamp queries',\n });\n\n // Create unique indexes\n for (const [columnName, columnDef] of Object.entries(columns)) {\n if (columnDef.unique && !columnDef.primaryKey) {\n indexes.push({\n name: `idx_${tableName}_${columnName}_unique`,\n columns: [columnName],\n unique: true,\n description: `Unique index for ${columnName}`,\n });\n }\n }\n\n return indexes;\n }\n\n /**\n * Generate trigger definitions for automatic timestamp updates\n */\n private generateTriggers(\n _objectDef: SmartObjectDefinition,\n tableName: string,\n ): TriggerDefinition[] {\n return [\n {\n name: `trg_${tableName}_updated_at`,\n when: 'BEFORE',\n event: 'UPDATE',\n body: `UPDATE ${quoteIdentifier(tableName)} SET \"updated_at\" = current_timestamp WHERE \"id\" = NEW.\"id\";`,\n description: 'Automatically update updated_at timestamp',\n },\n ];\n }\n\n /**\n * Extract foreign key definitions\n */\n private extractForeignKeys(\n columns: Record<string, ColumnDefinition>,\n ): ForeignKeyDefinition[] {\n const foreignKeys: ForeignKeyDefinition[] = [];\n\n for (const [columnName, columnDef] of Object.entries(columns)) {\n if (columnDef.foreignKey) {\n foreignKeys.push({\n column: columnName,\n referencesTable: columnDef.foreignKey.table,\n referencesColumn: columnDef.foreignKey.column,\n onDelete: columnDef.foreignKey.onDelete,\n onUpdate: columnDef.foreignKey.onUpdate,\n });\n }\n }\n\n return foreignKeys;\n }\n\n /**\n * Extract schema dependencies from foreign keys and inheritance\n */\n private extractDependencies(\n objectDef: SmartObjectDefinition,\n foreignKeys: ForeignKeyDefinition[],\n ): string[] {\n const dependencies = new Set<string>();\n\n // Add dependencies from foreign keys\n for (const fk of foreignKeys) {\n dependencies.add(fk.referencesTable);\n }\n\n // Add dependencies from base class\n if (\n objectDef.extends &&\n objectDef.extends !== 'SmrtObject' &&\n objectDef.extends !== 'SmrtCollection'\n ) {\n dependencies.add(this.classNameToTableName(objectDef.extends));\n }\n\n return Array.from(dependencies);\n }\n\n /**\n * Generate version hash for schema\n */\n private generateVersion(objectDef: SmartObjectDefinition): string {\n const content = JSON.stringify({\n className: objectDef.className,\n fields: objectDef.fields,\n extends: objectDef.extends,\n });\n return createHash('sha256').update(content).digest('hex').substring(0, 8);\n }\n\n /**\n * Get table name from object definition\n */\n private getTableName(objectDef: SmartObjectDefinition): string {\n return this.classNameToTableName(objectDef.className);\n }\n\n /**\n * Convert class name to table name (camelCase to snake_case, pluralized)\n */\n private classNameToTableName(className: string): string {\n return classnameToTablename(className);\n }\n\n /**\n * Extract package name from file path\n */\n private extractPackageName(filePath: string): string {\n const match = filePath.match(/packages\\/([^/]+)/);\n return match ? match[1] : 'unknown';\n }\n\n /**\n * Generate schema definition from ObjectRegistry fields (runtime)\n *\n * This method builds a SchemaDefinition from ObjectRegistry cached fields,\n * enabling schema generation from decorated classes at runtime.\n *\n * @param className - Class name to look up in ObjectRegistry\n * @param tableName - Table name (from SMRT_TABLE_NAME or derived)\n * @param fields - Map of Field definitions from ObjectRegistry\n * @returns Schema definition object\n */\n generateSchemaFromRegistry(\n className: string,\n tableName: string,\n fields: Map<string, RegistryField>,\n config?: SchemaGeneratorConfig,\n ): SchemaDefinition {\n const columns: Record<string, ColumnDefinition> = {};\n\n // Check for custom primary key\n let hasCustomPK = false;\n for (const [_fieldName, field] of fields.entries()) {\n if (field._meta?.primaryKey) {\n hasCustomPK = true;\n break;\n }\n }\n\n // Add default SMRT fields if no custom primary key\n if (!hasCustomPK) {\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n description: 'Primary identifier',\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n description: 'URL-friendly identifier',\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n defaultValue: '',\n description: 'Context for slug scoping',\n };\n }\n\n // Track timestamp fields to avoid duplicates\n let hasCreatedAt = false;\n let hasUpdatedAt = false;\n // Track regular (column-backed) fields that opted into a plain column index\n // via `indexed: true`. FK, unique, and meta fields each go through their\n // own dedicated index emission paths and are excluded from this set.\n const indexedColumns = new Set<string>();\n\n // Add fields from ObjectRegistry\n for (const [fieldName, field] of fields.entries()) {\n // Skip transient fields (non-persisted)\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n const isDefaultSmrtColumn =\n fieldName === 'id' || fieldName === 'slug' || fieldName === 'context';\n\n // Default SMRT columns are added above when we use the standard primary\n // key path. When a class declares a custom primary key, only explicitly\n // declared id/slug/context fields should survive this loop.\n if (\n isDefaultSmrtColumn &&\n (!hasCustomPK || field._meta?.__smrtSystemField === true)\n ) {\n continue;\n }\n\n // Track timestamp fields\n if (fieldName === 'created_at' || fieldName === 'createdAt') {\n if (hasCreatedAt) continue;\n hasCreatedAt = true;\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n continue;\n }\n if (fieldName === 'updated_at' || fieldName === 'updatedAt') {\n if (hasUpdatedAt) continue;\n hasUpdatedAt = true;\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n continue;\n }\n\n // Skip relationship fields that don't create columns\n // oneToMany and manyToMany are relationship metadata, not actual database columns\n // foreignKey DOES create a column (it stores the foreign key ID)\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column (STI only)\n // In CTI mode, meta fields shouldn't be used, but skip them anyway for safety\n if (field.type === 'meta') {\n continue;\n }\n\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n // If _meta.nullable is true, the field can be null regardless of required\n notNull: field._meta?.nullable ? false : field._meta?.required || false,\n primaryKey: field._meta?.primaryKey || false,\n unique: field._meta?.unique || false,\n description: field._meta?.description,\n };\n\n // Get default value\n if (\n field._meta?.default !== undefined &&\n this.shouldEmitDefault(field, field._meta.default)\n ) {\n columnDef.defaultValue = field._meta.default;\n }\n // Note: Removed automatic NOT NULL DEFAULT '' for TEXT columns\n // This was forcing all TEXT fields to be NOT NULL regardless of required option\n // If DuckDB ANY type inference is an issue, it should be handled at the adapter level\n // or with an explicit field option, not by silently changing schema semantics\n\n // Handle foreign keys\n if (field.type === 'foreignKey') {\n // Type cast to access relationship-specific properties\n const relatedName = field.related; // Top-level property\n const onDeleteAction = field._meta?.onDelete;\n\n if (relatedName) {\n columnDef.foreignKey = {\n table: this.classNameToTableName(relatedName),\n column: 'id',\n onDelete: onDeleteAction || 'CASCADE',\n onUpdate: 'CASCADE',\n };\n }\n }\n\n // Track opt-in column indexes for regular (non-FK, non-unique) fields.\n // FK columns already get auto-indexed below; unique columns produce\n // their own unique index; meta fields go through the jsonPath path.\n const isIndexed = field._meta?.indexed === true || field.indexed === true;\n if (\n isIndexed &&\n !columnDef.foreignKey &&\n !columnDef.unique &&\n !columnDef.primaryKey\n ) {\n indexedColumns.add(this.toSnakeCase(fieldName));\n }\n\n columns[this.toSnakeCase(fieldName)] = columnDef;\n }\n\n // Ensure timestamp columns exist\n if (!hasCreatedAt) {\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n }\n if (!hasUpdatedAt) {\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n }\n\n this.reconcileRegistryForeignKeyColumnTypes(\n columns,\n fields,\n config?.registry,\n );\n\n // Generate indexes\n const indexes: IndexDefinition[] = [];\n\n if (!hasCustomPK) {\n indexes.push({\n name: `${tableName}_id_idx`,\n columns: ['id'],\n description: 'Primary key index',\n });\n\n const conflictColumns = config?.conflictColumns || ['slug', 'context'];\n const conflictIndexName =\n conflictColumns.length > 2\n ? `${tableName}_${conflictColumns.slice(0, 2).join('_')}_idx`\n : `${tableName}_${conflictColumns.join('_')}_idx`;\n\n indexes.push({\n name: conflictIndexName,\n columns: conflictColumns,\n unique: true,\n description: `Unique conflict index for ${className}`,\n });\n } else {\n // Find custom PK column and create index\n for (const [colName, colDef] of Object.entries(columns)) {\n if (colDef.primaryKey) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n description: `Primary key index`,\n });\n break;\n }\n }\n }\n\n // Create indexes for foreign keys\n for (const [colName, colDef] of Object.entries(columns)) {\n if (colDef.foreignKey) {\n indexes.push({\n name: `idx_${tableName}_${colName}`,\n columns: [colName],\n description: `Foreign key index for ${colName}`,\n });\n }\n }\n\n // Emit opt-in column indexes for regular fields tagged with `indexed: true`\n for (const colName of indexedColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n description: `Index for ${colName}`,\n });\n }\n\n return {\n tableName,\n columns,\n indexes,\n triggers: [],\n foreignKeys: this.extractForeignKeys(columns),\n dependencies: [],\n version: createHash('sha256')\n .update(JSON.stringify(columns))\n .digest('hex')\n .substring(0, 8),\n packageName: 'runtime',\n baseClass: 'SmrtObject',\n };\n }\n\n /**\n * Generate STI (Single Table Inheritance) schema from ObjectRegistry fields\n *\n * Creates a shared table for an inheritance hierarchy with:\n * - _meta_type: Discriminator column to identify class type\n * - _meta_data: JSON column for flexible field storage\n * - Union of all FK columns from descendants (all nullable)\n * - Partial indexes for FK columns (filtered by _meta_type)\n *\n * @param baseClassName - Base class name for the STI hierarchy\n * @param tableName - Shared table name (from base class)\n * @param fields - Map of Field definitions from base class\n * @returns Schema definition object for STI table\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = '';\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * roomId = foreignKey(Room);\n * }\n *\n * // Generates single table 'events' with:\n * // - title (from Event)\n * // - room_id (from Meeting, nullable)\n * // - _meta_type TEXT NOT NULL (discriminator)\n * // - _meta_data JSON (flexible storage)\n * ```\n */\n async generateSTISchemaFromRegistry(\n baseClassName: string,\n tableName: string,\n _fields: Map<string, RegistryField>,\n config?: SchemaGeneratorConfig,\n ): Promise<SchemaDefinition> {\n const ObjectRegistry =\n config?.registry ?? (await import('../registry.js')).ObjectRegistry;\n const columns: Record<string, ColumnDefinition> = {};\n\n // Add default SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n description: 'Primary identifier',\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n description: 'URL-friendly identifier',\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n defaultValue: '',\n description: 'Context for slug scoping',\n };\n\n // Add STI discriminator column\n columns._meta_type = {\n type: 'TEXT',\n notNull: true,\n description: 'Class type discriminator for STI',\n };\n\n // Add meta column for flexible storage\n columns._meta_data = {\n type: 'JSON',\n notNull: false,\n description: 'Flexible JSON storage for meta() fields',\n };\n\n // Track timestamp fields to avoid duplicates\n let hasCreatedAt = false;\n let hasUpdatedAt = false;\n\n // Get all descendants to aggregate fields\n const descendants = ObjectRegistry.getDescendants(baseClassName);\n const allClassNames = [baseClassName, ...descendants];\n\n // Track FK columns by class for partial indexes\n const fkColumnsByClass = new Map<string, Set<string>>();\n // Track meta fields opted into JSON-path indexing (deduped across STI subtypes)\n const indexedMetaFields = new Set<string>();\n // Track regular-field columns opted into plain column indexing\n const indexedStiColumns = new Set<string>();\n\n // Aggregate fields from base and all descendants\n for (const className of allClassNames) {\n const classFields: Map<string, RegistryField> =\n await ObjectRegistry.getAllFields(className);\n fkColumnsByClass.set(className, new Set());\n\n for (const [fieldName, field] of classFields.entries()) {\n // Skip transient fields\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n // Capture indexed meta fields before the skip-meta branch below\n if (\n field.type === 'meta' &&\n (field.indexed === true || field._meta?.indexed === true)\n ) {\n indexedMetaFields.add(fieldName);\n }\n\n // Skip default fields already added\n if (\n fieldName === 'id' ||\n fieldName === 'slug' ||\n fieldName === 'context'\n ) {\n continue;\n }\n\n // Track timestamp fields\n if (fieldName === 'created_at' || fieldName === 'createdAt') {\n if (hasCreatedAt) continue;\n hasCreatedAt = true;\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n continue;\n }\n if (fieldName === 'updated_at' || fieldName === 'updatedAt') {\n if (hasUpdatedAt) continue;\n hasUpdatedAt = true;\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n continue;\n }\n\n // Skip relationship fields that don't create columns\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column\n if (field.type === 'meta') {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n\n // Check if column already exists (inherited from parent)\n if (columns[columnName]) {\n continue;\n }\n\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n // STI: All columns nullable (union of child fields)\n notNull: false,\n primaryKey: false,\n unique: false,\n description: field._meta?.description,\n };\n\n // Get default value (but not applied in STI - defaults handled by application)\n if (\n field._meta?.default !== undefined &&\n this.shouldEmitDefault(field, field._meta.default)\n ) {\n columnDef.defaultValue = field._meta.default;\n }\n\n // Handle foreign keys\n if (field.type === 'foreignKey') {\n const relatedName = field.related; // Top-level property\n const onDeleteAction = field._meta?.onDelete;\n\n if (relatedName) {\n columnDef.foreignKey = {\n table: this.classNameToTableName(relatedName),\n column: 'id',\n onDelete: onDeleteAction || 'CASCADE',\n onUpdate: 'CASCADE',\n };\n\n // Track FK column for this class (for partial indexes)\n fkColumnsByClass.get(className)?.add(columnName);\n }\n }\n\n // Track opt-in column indexes for regular STI columns\n const isIndexed =\n field._meta?.indexed === true || field.indexed === true;\n if (isIndexed && !columnDef.foreignKey) {\n indexedStiColumns.add(columnName);\n }\n\n columns[columnName] = columnDef;\n }\n }\n\n // Ensure timestamp columns exist\n if (!hasCreatedAt) {\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n }\n if (!hasUpdatedAt) {\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n }\n\n for (const className of allClassNames) {\n const classFields = await ObjectRegistry.getAllFields(className);\n this.reconcileRegistryForeignKeyColumnTypes(\n columns,\n classFields,\n ObjectRegistry,\n );\n }\n\n // Generate indexes\n const indexes: IndexDefinition[] = [];\n\n // Primary key index\n indexes.push({\n name: `${tableName}_id_idx`,\n columns: ['id'],\n description: 'Primary key index',\n });\n\n // Unique index for slug, context, and type (STI variation).\n // STI subclasses share a table, so the discriminator participates in\n // identity — two subtypes can share the same (slug, context). PostgreSQL\n // UPSERT requires the live schema to carry a matching unique index; the\n // migration differ repairs older deployments where this index was created\n // non-unique or never created at all (issue #1165).\n indexes.push({\n name: `${tableName}_slug_context_meta_type_idx`,\n columns: ['slug', 'context', '_meta_type'],\n unique: true,\n description: 'Unique index for slug, context, and type',\n });\n\n // Index on type column (for polymorphic queries)\n indexes.push({\n name: `${tableName}_meta_type_idx`,\n columns: ['_meta_type'],\n description: 'Index for type discriminator queries',\n });\n\n // Partial indexes for FK columns (filtered by type)\n for (const [className, fkColumns] of fkColumnsByClass.entries()) {\n for (const fkColumn of fkColumns) {\n indexes.push({\n name: `idx_${tableName}_${fkColumn}_${className.toLowerCase()}`,\n columns: [fkColumn],\n where: `_meta_type = ${quoteStringLiteral(className)}`,\n description: `Partial index for ${fkColumn} in ${className} rows`,\n });\n }\n }\n\n // JSON-path indexes for @meta({ indexed: true }) fields\n for (const fieldName of indexedMetaFields) {\n indexes.push({\n name: `${tableName}_meta_${this.toSnakeCase(fieldName)}_idx`,\n columns: [],\n jsonPath: { column: '_meta_data', path: fieldName },\n description: `JSON-path index for @meta({ indexed: true }) field ${fieldName}`,\n });\n }\n\n // Plain column indexes for regular STI columns opted in via `indexed: true`\n for (const colName of indexedStiColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n description: `Index for ${colName}`,\n });\n }\n\n return {\n tableName,\n columns,\n indexes,\n triggers: [],\n foreignKeys: this.extractForeignKeys(columns),\n dependencies: [],\n version: createHash('sha256')\n .update(JSON.stringify({ columns, baseClassName, descendants }))\n .digest('hex')\n .substring(0, 8),\n packageName: 'runtime',\n baseClass: 'SmrtObject',\n };\n }\n\n /**\n * Generate STI schema from manifest data (build-time, no runtime registry)\n *\n * This method generates STI schema purely from manifest data, without depending\n * on ObjectRegistry. This enables pre-generating schemas at build time for\n * efficient external package consumption.\n *\n * @param baseClassName - Base class name for the STI hierarchy\n * @param tableName - Shared table name (from base class)\n * @param baseFields - Fields from the base class\n * @param manifest - Full manifest containing all objects\n * @returns ManifestSchema for storage in manifest.json\n */\n generateSTISchemaFromManifest(\n baseClassName: string,\n tableName: string,\n _baseFields: Record<string, FieldDefinition>,\n manifest: SmartObjectManifest,\n config?: SchemaGeneratorConfig,\n ): ManifestSchema {\n const columns: Record<string, ManifestColumnDefinition> = {};\n\n // Add default SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n default: '',\n };\n\n // Add STI discriminator column\n columns._meta_type = {\n type: 'TEXT',\n notNull: true,\n };\n\n // Add meta column for flexible storage\n columns._meta_data = {\n type: 'JSON',\n notNull: false,\n };\n\n // Timestamp fields\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n // Find all descendants in manifest\n const descendants = this.findDescendantsInManifest(baseClassName, manifest);\n const allClassNames = [baseClassName, ...descendants];\n\n // Track FK columns by class for partial indexes\n const fkColumnsByClass = new Map<string, Set<string>>();\n // Track meta fields opted into JSON-path indexing (deduped across STI subtypes)\n const indexedMetaFields = new Set<string>();\n // Track regular columns opted into plain column indexing\n const indexedStiColumns = new Set<string>();\n\n // Aggregate fields from base and all descendants\n for (const className of allClassNames) {\n const objDef = manifest.objects[className];\n if (!objDef) continue;\n\n fkColumnsByClass.set(className, new Set());\n\n for (const [fieldName, field] of Object.entries(objDef.fields)) {\n // Skip transient fields\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n // Capture indexed meta fields before the skip-meta branch below.\n // `indexed` is normally carried in `_meta`, but tolerate a legacy\n // top-level flag too — RegistryField models both placements.\n const indexedField: RegistryField = field;\n if (\n field.type === 'meta' &&\n (indexedField.indexed === true || field._meta?.indexed === true)\n ) {\n indexedMetaFields.add(fieldName);\n }\n\n // Skip default fields already added\n if (\n fieldName === 'id' ||\n fieldName === 'slug' ||\n fieldName === 'context' ||\n fieldName === 'created_at' ||\n fieldName === 'createdAt' ||\n fieldName === 'updated_at' ||\n fieldName === 'updatedAt'\n ) {\n continue;\n }\n\n // Skip relationship fields that don't create columns\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column\n if (field.type === 'meta') {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n\n // Check if column already exists (inherited from parent)\n if (columns[columnName]) {\n continue;\n }\n\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ManifestColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n // STI: All columns nullable (union of child fields)\n notNull: false,\n };\n\n // Get default value\n if (\n field.default !== undefined &&\n this.shouldEmitDefault(field, field.default)\n ) {\n columnDef.default = field.default;\n }\n\n // Track FK columns for this class (for partial indexes)\n if (field.type === 'foreignKey') {\n fkColumnsByClass.get(className)?.add(columnName);\n }\n\n // Track opt-in column indexes for regular STI columns\n const isIndexed =\n indexedField.indexed === true || field._meta?.indexed === true;\n if (isIndexed && field.type !== 'foreignKey') {\n indexedStiColumns.add(columnName);\n }\n\n columns[columnName] = columnDef;\n }\n }\n\n // Generate indexes\n const indexes: ManifestIndexDefinition[] = [];\n\n // Primary key index\n indexes.push({\n name: `${tableName}_id_idx`,\n columns: ['id'],\n });\n\n // Unique index for slug, context, and type (STI variation) — see\n // generateSTISchemaFromRegistry for the rationale.\n indexes.push({\n name: `${tableName}_slug_context_meta_type_idx`,\n columns: ['slug', 'context', '_meta_type'],\n unique: true,\n });\n\n // Index on type column (for polymorphic queries)\n indexes.push({\n name: `${tableName}_meta_type_idx`,\n columns: ['_meta_type'],\n });\n\n // JSON-path indexes for @meta({ indexed: true }) fields\n for (const fieldName of indexedMetaFields) {\n indexes.push({\n name: `${tableName}_meta_${this.toSnakeCase(fieldName)}_idx`,\n columns: [],\n jsonPath: { column: '_meta_data', path: fieldName },\n });\n }\n\n // Plain column indexes for regular STI columns opted in via `indexed: true`\n for (const colName of indexedStiColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n });\n }\n\n // Generate DDL\n const schemaDefinition: SchemaDefinition = {\n tableName,\n columns: this.convertManifestColumnsToSchemaColumns(columns),\n indexes: indexes.map((idx) => ({\n name: idx.name,\n columns: idx.columns,\n unique: idx.unique,\n where: idx.where,\n jsonPath: idx.jsonPath,\n })),\n triggers: [],\n foreignKeys: [],\n dependencies: [],\n version: '',\n packageName: '',\n };\n\n const ddl = this.generateSQL(schemaDefinition);\n\n // Generate version hash\n const version = createHash('sha256')\n .update(JSON.stringify({ columns, baseClassName, descendants }))\n .digest('hex')\n .substring(0, 8);\n\n return {\n tableName,\n ddl,\n columns,\n indexes,\n version,\n };\n }\n\n /**\n * Generate CTI (Class Table Inheritance) schema from manifest data\n *\n * @param className - Class name\n * @param tableName - Table name\n * @param fields - Fields from the class\n * @returns ManifestSchema for storage in manifest.json\n */\n generateCTISchemaFromManifest(\n className: string,\n tableName: string,\n fields: Record<string, FieldDefinition>,\n config?: SchemaGeneratorConfig,\n ): ManifestSchema {\n const columns: Record<string, ManifestColumnDefinition> = {};\n\n // Track regular columns opted into a plain index via `@field({ indexed: true })`.\n // Mirrors the STI path (generateSTISchemaFromManifest) so CTI tables honor\n // the documented `FieldOptions.indexed` flag too.\n const indexedColumns = new Set<string>();\n\n // Add default SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n default: '',\n };\n\n // Timestamp fields\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n // Add class fields\n for (const [fieldName, field] of Object.entries(fields)) {\n // Skip transient fields\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n // Skip default fields already added\n if (\n fieldName === 'id' ||\n fieldName === 'slug' ||\n fieldName === 'context' ||\n fieldName === 'created_at' ||\n fieldName === 'createdAt' ||\n fieldName === 'updated_at' ||\n fieldName === 'updatedAt'\n ) {\n continue;\n }\n\n // Skip relationship fields that don't create columns\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields\n if (field.type === 'meta') {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ManifestColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n notNull: field._meta?.nullable ? false : field.required || false,\n unique: field._meta?.unique || false,\n };\n\n if (\n field.default !== undefined &&\n this.shouldEmitDefault(field, field.default)\n ) {\n columnDef.default = field.default;\n }\n\n // Opt-in plain column index. FK columns and unique columns get their own\n // indexes, so skip those here.\n const indexedField: RegistryField = field;\n const isIndexed =\n indexedField.indexed === true || field._meta?.indexed === true;\n if (isIndexed && field.type !== 'foreignKey' && !columnDef.unique) {\n indexedColumns.add(columnName);\n }\n\n columns[columnName] = columnDef;\n }\n\n // Generate indexes\n const indexes: ManifestIndexDefinition[] = [];\n\n indexes.push({\n name: `${tableName}_id_idx`,\n columns: ['id'],\n });\n\n // Use custom conflict columns if provided, otherwise default to slug+context\n const conflictColumns = config?.conflictColumns || ['slug', 'context'];\n const indexName =\n conflictColumns.length > 2\n ? `${tableName}_${conflictColumns.slice(0, 2).join('_')}_idx`\n : `${tableName}_${conflictColumns.join('_')}_idx`;\n\n indexes.push({\n name: indexName,\n columns: conflictColumns,\n unique: true,\n });\n\n // Plain column indexes for regular columns opted in via `indexed: true`.\n for (const colName of indexedColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n });\n }\n\n // Generate DDL\n const schemaDefinition: SchemaDefinition = {\n tableName,\n columns: this.convertManifestColumnsToSchemaColumns(columns),\n indexes: indexes.map((idx) => ({\n name: idx.name,\n columns: idx.columns,\n unique: idx.unique,\n where: idx.where,\n jsonPath: idx.jsonPath,\n })),\n triggers: [],\n foreignKeys: [],\n dependencies: [],\n version: '',\n packageName: '',\n };\n\n const ddl = this.generateSQL(schemaDefinition);\n\n // Generate version hash\n const version = createHash('sha256')\n .update(JSON.stringify({ columns, className }))\n .digest('hex')\n .substring(0, 8);\n\n return {\n tableName,\n ddl,\n columns,\n indexes,\n version,\n };\n }\n\n /**\n * Find all descendants of a class in the manifest\n *\n * Note: Manifest keys are now qualified names (@pkg:ClassName) but `extends` field\n * stores simple PascalCase class names. We need to handle both qualified and simple\n * name lookups.\n *\n * Issue #713: Updated to handle qualified names in manifest keys\n */\n private findDescendantsInManifest(\n baseClassName: string,\n manifest: SmartObjectManifest,\n visited: Set<string> = new Set(),\n ): string[] {\n const descendants: string[] = [];\n\n // Prevent infinite recursion (e.g., class extends same-named class from another package)\n if (visited.has(baseClassName)) return descendants;\n visited.add(baseClassName);\n\n // Extract the simple class name from qualified name if needed\n // e.g., '@happyvertical/smrt-core:PolyEvent' -> 'PolyEvent'\n const simpleBaseClassName = baseClassName.includes(':')\n ? baseClassName.split(':').pop() || baseClassName\n : baseClassName;\n const baseClassLower = simpleBaseClassName.toLowerCase();\n\n for (const [name, obj] of Object.entries(manifest.objects)) {\n // Skip self-references (class extending same-named class from another package)\n if (\n obj.className.toLowerCase() === baseClassLower &&\n obj.extends?.toLowerCase() === baseClassLower\n ) {\n continue;\n }\n // obj.extends is a simple class name (e.g., 'PolyEvent')\n // Compare with the simple (non-qualified) version of the base class name\n if (obj.extends?.toLowerCase() === baseClassLower) {\n descendants.push(name);\n // Recursively find descendants of this class\n // Pass the qualified name (manifest key) for recursive lookup\n descendants.push(\n ...this.findDescendantsInManifest(name, manifest, visited),\n );\n }\n }\n\n return descendants;\n }\n\n /**\n * Convert ManifestColumnDefinition to ColumnDefinition\n */\n private convertManifestColumnsToSchemaColumns(\n manifestColumns: Record<string, ManifestColumnDefinition>,\n ): Record<string, ColumnDefinition> {\n const columns: Record<string, ColumnDefinition> = {};\n\n for (const [name, col] of Object.entries(manifestColumns)) {\n columns[name] = {\n type: col.type as SQLDataType,\n primaryKey: col.primaryKey,\n referenceKind: col.referenceKind,\n notNull: col.notNull,\n unique: col.unique,\n defaultValue: col.default,\n };\n }\n\n return columns;\n }\n\n /**\n * Convert camelCase to snake_case\n */\n private toSnakeCase(str: string): string {\n return str\n .replace(/([A-Z])/g, '_$1')\n .toLowerCase()\n .replace(/^_/, '');\n }\n\n /**\n * Generate SQL CREATE TABLE statement from schema definition\n *\n * This is the single source of truth for SQL generation, consolidating\n * logic that was previously duplicated across multiple code paths.\n *\n * @param schema - Schema definition object\n * @returns SQL CREATE TABLE statement with indexes\n */\n generateSQL(schema: SchemaDefinition, engine?: DatabaseEngine): string {\n // NOTE: We no longer append indexes to DDL string here.\n // The SDK expects ddl to contain ONLY the CREATE TABLE statement.\n // Indexes are stored separately in schema.indexes as SQL strings\n // and the SDK handles them via syncSchema() or dedicated index creation.\n if (engine) {\n return getDDLStrategy(engine).generateCreateTable(schema);\n }\n\n const { tableName, columns } = schema;\n let sql = `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(tableName)} (\\n`;\n\n for (const [columnName, columnDef] of Object.entries(columns)) {\n const parts = [` ${quoteIdentifier(columnName)} ${columnDef.type}`];\n\n if (columnDef.primaryKey) {\n parts.push('PRIMARY KEY');\n }\n\n if (columnDef.notNull) {\n parts.push('NOT NULL');\n }\n\n if (columnDef.unique && !columnDef.primaryKey) {\n parts.push('UNIQUE');\n }\n\n if (columnDef.defaultValue !== undefined) {\n parts.push(\n `DEFAULT ${this.formatDefaultValue(\n columnDef.defaultValue,\n columnDef.type,\n )}`,\n );\n }\n\n sql += `${parts.join(' ')},\\n`;\n }\n\n return `${sql.slice(0, -2)}\\n);`;\n }\n\n /**\n * Format default value for SQL\n *\n * SQLite doesn't support CAST expressions in DEFAULT values (only literal values are allowed).\n * This method generates DEFAULT values that work with both SQLite and DuckDB.\n *\n * @param value - Default value\n * @param type - Column SQL type\n * @returns Formatted SQL default value expression\n */\n private formatDefaultValue(value: unknown, type: SQLDataType): string {\n // Delegate to the shared, injection-safe formatter so all DDL paths\n // converge on one set of rules (allowlisted keyword/function defaults\n // instead of \"contains `(`\", type-driven quoting, no string-\"null\" fold).\n // This path is engine-agnostic and never emits CAST expressions, so the\n // output stays valid for SQLite and DuckDB. Non-string TIMESTAMP defaults\n // fall back to lowercase `current_timestamp` to preserve prior output.\n return formatDefaultValueShared(value, type, {\n nonStringTimestampDefault: 'current_timestamp',\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generator.js","names":[],"sources":["../../src/schema/generator.ts"],"sourcesContent":["/**\n * Schema generator for SMRT objects\n * Converts AST field definitions to database schema definitions\n */\n\nimport { createHash } from 'node:crypto';\nimport type {\n FieldDefinition,\n FieldMeta,\n ManifestColumnDefinition,\n ManifestIndexDefinition,\n ManifestSchema,\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types.js';\nimport { classnameToTablename } from '../utils/naming.js';\nimport {\n type ConflictTableStrategy,\n conflictIndexName,\n resolveConflictColumns,\n resolveTenantColumn,\n servesSlugLookup,\n} from './conflict-target.js';\nimport { getDDLStrategy } from './ddl/index.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n assertIdentifierFits,\n enforceIdentifierLimits,\n shortenIdentifier,\n} from './index-utils.js';\nimport {\n formatDefaultValue as formatDefaultValueShared,\n quoteIdentifier,\n quoteStringLiteral,\n} from './sql-identifiers.js';\nimport type {\n ColumnDefinition,\n DeclaredIndexDefinition,\n ForeignKeyDefinition,\n IndexDefinition,\n SchemaDefinition,\n SQLDataType,\n TriggerDefinition,\n} from './types.js';\n\n/**\n * Structural shape of a field as read from either the build-time manifest\n * (`FieldDefinition`) or the runtime ObjectRegistry (`getAllFields()` returns\n * `Map<string, any>` upstream). The schema generator consumes both through a\n * single set of property reads, so this interface captures every field key the\n * generator touches without coupling to either source's exact type.\n *\n * `_meta` carries the structured field-helper metadata (`FieldMeta`); a few\n * legacy fields (`indexed`, `idType`, `__tenancy`) may also appear at the top\n * level depending on the source, so they are declared here too.\n */\ninterface RegistryField {\n type?: FieldDefinition['type'];\n related?: string;\n required?: boolean;\n default?: unknown;\n description?: string;\n transient?: boolean;\n indexed?: boolean;\n idType?: 'uuid' | 'text';\n __tenancy?: FieldMeta['__tenancy'];\n _meta?: FieldMeta;\n}\n\ntype SchemaGeneratorConfig = {\n /**\n * The table's resolved conflict target. Registry callers pass\n * `ObjectRegistry.getConflictColumns()`; the manifest path passes the\n * object's `decoratorConfig` after `ManifestGenerator.normalizeConflictColumns()`\n * materialized the tenant-aware default (#2360). When absent, the generator\n * derives the default itself from `tenantScoped` and the strategy.\n */\n conflictColumns?: string[];\n /**\n * The schema owner's `@smrt({ tenantScoped })` config as the manifest\n * carries it — consulted only to derive the default conflict target when\n * `conflictColumns` is absent.\n */\n tenantScoped?: boolean | { field?: string };\n idType?: 'uuid' | 'text';\n /**\n * `@smrt({ indexes: [...] })` declarations (#2357).\n *\n * The manifest paths receive the whole `decoratorConfig`, so this arrives for\n * free there. The runtime paths rebuild a narrow config bag by hand\n * (`schema/utils.ts`, `testing/database.ts`) and must thread it explicitly —\n * omitting it is what made the option unreachable at runtime.\n */\n indexes?: DeclaredIndexDefinition[];\n registry?: {\n getConfig?(className: string): { idType?: 'uuid' | 'text' };\n getDescendants(baseClassName: string): string[];\n getAllFields(className: string): Promise<Map<string, RegistryField>>;\n getSTIBase?(className: string): string | null;\n };\n};\n\nexport class SchemaGenerator {\n /**\n * Generate schema definition from SMRT object definition\n */\n generateSchema(objectDef: SmartObjectDefinition): SchemaDefinition {\n const tableName = this.getTableName(objectDef);\n const columns = this.generateColumns(\n objectDef.fields,\n objectDef.decoratorConfig,\n );\n const indexes = this.generateIndexes(objectDef, columns);\n const triggers = this.generateTriggers(objectDef, tableName);\n const foreignKeys = this.extractForeignKeys(columns);\n const dependencies = this.extractDependencies(objectDef, foreignKeys);\n const version = this.generateVersion(objectDef);\n\n // The default list page orders by created_at (#2363) — after the declared\n // indexes generateIndexes() appended, before the reference-column pass so\n // the composite suppresses a standalone tenant index.\n this.ensureDefaultListOrderingIndex(indexes, columns, tableName);\n\n // Polymorphic association owner lookup `(meta_type, meta_id)` (#2364,\n // epic #2382 finding A3) — alongside the default-ordering composite,\n // before the reference-column pass.\n this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);\n\n // Reference columns (@foreignKey / @crossPackageRef / tenant_id) are\n // always indexed (#2356, #2359).\n this.ensureReferenceColumnIndexes(indexes, columns, tableName);\n\n // Last: nothing may lengthen a name after this (#2374).\n enforceIdentifierLimits(tableName, indexes);\n for (const trigger of triggers) {\n trigger.name = shortenIdentifier(trigger.name);\n }\n\n return {\n tableName,\n columns,\n indexes,\n triggers,\n foreignKeys,\n dependencies,\n version,\n packageName: this.extractPackageName(objectDef.filePath),\n baseClass: objectDef.extends,\n };\n }\n\n /**\n * Convert field type to SQL data type\n */\n private mapFieldTypeToSQL(fieldType: FieldDefinition['type']): SQLDataType {\n switch (fieldType) {\n case 'text':\n return 'TEXT';\n case 'integer':\n return 'INTEGER';\n case 'decimal':\n return 'REAL';\n case 'boolean':\n return 'BOOLEAN';\n case 'datetime':\n return 'TIMESTAMP';\n case 'json':\n return 'JSON';\n case 'foreignKey':\n return 'UUID'; // Foreign keys default to UUID, then same-package refs can be reconciled to target idType\n case 'crossPackageRef':\n return 'UUID'; // Cross-package refs are UUID ids with no DDL FK constraint\n default:\n return 'TEXT'; // Default fallback\n }\n }\n\n private getIdColumnType(config?: { idType?: 'uuid' | 'text' }): SQLDataType {\n return config?.idType === 'text' ? 'TEXT' : 'UUID';\n }\n\n private getRelationshipColumnType(field: RegistryField): SQLDataType {\n if (field?._meta?.sqlType) {\n return String(field._meta.sqlType).toUpperCase() as SQLDataType;\n }\n\n if (\n field?.type === 'crossPackageRef' &&\n (field._meta?.idType === 'text' || field.idType === 'text')\n ) {\n return 'TEXT';\n }\n\n return this.mapFieldTypeToSQL(field?.type || 'text');\n }\n\n private getReferenceKind(\n field: RegistryField,\n ): ColumnDefinition['referenceKind'] | undefined {\n if (\n field?.__tenancy?.isTenantIdField ||\n field?._meta?.__tenancy?.isTenantIdField\n ) {\n return 'tenantId';\n }\n\n if (field?.type === 'foreignKey') {\n return 'foreignKey';\n }\n\n if (field?.type === 'crossPackageRef') {\n return 'crossPackageRef';\n }\n\n return undefined;\n }\n\n private shouldEmitDefault(\n field: RegistryField,\n defaultValue: unknown,\n ): boolean {\n return !(\n this.getReferenceKind(field) === 'tenantId' &&\n this.getRelationshipColumnType(field) === 'UUID' &&\n defaultValue === ''\n );\n }\n\n private getRegistryTargetIdColumnType(\n relatedName: string | undefined,\n registry: SchemaGeneratorConfig['registry'] | undefined,\n ): SQLDataType | undefined {\n if (!relatedName || !registry?.getConfig) {\n return undefined;\n }\n\n const targetName = relatedName.split('.')[0];\n const targetNames = [targetName];\n const stiBase = registry.getSTIBase?.(targetName);\n if (stiBase && !targetNames.includes(stiBase)) {\n targetNames.push(stiBase);\n }\n\n for (const name of targetNames) {\n const idType = registry.getConfig(name)?.idType;\n if (idType === 'text') {\n return 'TEXT';\n }\n if (idType === 'uuid') {\n return 'UUID';\n }\n }\n\n return undefined;\n }\n\n private reconcileRegistryForeignKeyColumnTypes(\n columns: Record<string, ColumnDefinition>,\n fields: Map<string, RegistryField>,\n registry: SchemaGeneratorConfig['registry'] | undefined,\n ): void {\n if (!registry?.getConfig) {\n return;\n }\n\n for (const [fieldName, field] of fields.entries()) {\n if (field.type !== 'foreignKey' || field._meta?.sqlType) {\n continue;\n }\n\n const targetIdType = this.getRegistryTargetIdColumnType(\n field.related,\n registry,\n );\n if (!targetIdType) {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n if (columns[columnName]) {\n columns[columnName] = {\n ...columns[columnName],\n type: targetIdType,\n };\n }\n }\n }\n\n /**\n * Generate column definitions\n */\n private generateColumns(\n fields: Record<string, FieldDefinition>,\n config?: { idType?: 'uuid' | 'text' },\n ): Record<string, ColumnDefinition> {\n const columns: Record<string, ColumnDefinition> = {};\n\n // Always include base SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n description: 'Primary identifier',\n };\n\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n\n // Add fields from object definition\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n // Skip id fields as we handle them above\n if (\n fieldName === 'id' ||\n fieldName === 'created_at' ||\n fieldName === 'updated_at'\n ) {\n continue;\n }\n\n // Skip transient fields (non-persisted)\n // NOTE: This filtering logic must match SmrtObject.toJSON()\n // Changes to field filtering should be applied in BOTH places\n if (fieldDef.transient || fieldDef._meta?.transient) {\n continue;\n }\n\n // Skip relationship fields that don't create columns\n // oneToMany and manyToMany are relationship metadata, not actual database columns\n // foreignKey DOES create a column (it stores the foreign key ID)\n // NOTE: This must match the filtering in SmrtObject.toJSON()\n if (fieldDef.type === 'oneToMany' || fieldDef.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column (STI only)\n // In CTI mode, meta fields shouldn't be used, but skip them anyway for safety\n if (fieldDef.type === 'meta') {\n continue;\n }\n\n const column: ColumnDefinition = {\n type: this.getRelationshipColumnType(fieldDef),\n referenceKind: this.getReferenceKind(fieldDef),\n // If _meta.nullable is true, the field can be null regardless of required\n // This handles field helpers like text({ required: true, nullable: true })\n notNull: fieldDef._meta?.nullable ? false : fieldDef.required || false,\n unique: fieldDef._meta?.unique || false,\n description: fieldDef.description,\n };\n\n // Handle default values\n if (\n fieldDef.default !== undefined &&\n this.shouldEmitDefault(fieldDef, fieldDef.default)\n ) {\n column.defaultValue = fieldDef.default;\n }\n // Note: Removed automatic NOT NULL DEFAULT '' for TEXT columns\n // This was forcing all TEXT fields to be NOT NULL regardless of required option\n // If DuckDB ANY type inference is an issue, it should be handled at the adapter level\n // or with an explicit field option, not by silently changing schema semantics\n\n // Handle foreign keys\n if (fieldDef.type === 'foreignKey' && fieldDef.related) {\n const [table, columnName = 'id'] = fieldDef.related.split('.');\n column.foreignKey = {\n table,\n column: columnName,\n onDelete: 'CASCADE', // Default behavior\n onUpdate: 'CASCADE',\n };\n }\n\n // Handle unique constraints\n if (fieldName === 'slug' || fieldName === 'email') {\n column.unique = true;\n }\n\n columns[fieldName] = column;\n }\n\n return columns;\n }\n\n /**\n * Whether an existing index already leads with `leadingColumns`, in order.\n *\n * Only an UNQUALIFIED index (no partial `WHERE`, no JSON-path expression)\n * counts. A partial index such as `... WHERE _meta_type = 'Article'` cannot\n * serve a base-class polymorphic query, which carries no subtype predicate,\n * so it must not suppress the standalone index (#2359, review of #2384).\n *\n * A B-tree serves any prefix of its column list, so an index over\n * `(tenant_id, created_at, status)` covers both `(tenant_id)` equality\n * lookups and `(tenant_id, created_at)` ordering — hence the prefix test\n * rather than an exact match (#2363).\n */\n private hasUnqualifiedLeadingColumns(\n indexes: ReadonlyArray<{\n columns: string[];\n where?: string;\n jsonPath?: unknown;\n }>,\n leadingColumns: readonly string[],\n ): boolean {\n return indexes.some(\n (index) =>\n !index.where &&\n !index.jsonPath &&\n (index.columns?.length ?? 0) >= leadingColumns.length &&\n leadingColumns.every((column, i) => index.columns[i] === column),\n );\n }\n\n /**\n * Whether an existing index already serves equality lookups on `column`.\n *\n * Single-column shorthand for {@link hasUnqualifiedLeadingColumns}.\n */\n private hasUnqualifiedLeadingIndex(\n indexes: ReadonlyArray<{\n columns: string[];\n where?: string;\n jsonPath?: unknown;\n }>,\n column: string,\n ): boolean {\n return this.hasUnqualifiedLeadingColumns(indexes, [column]);\n }\n\n /**\n * The column list of the default list-ordering index for a table.\n *\n * Every generated list surface — REST, MCP, and the SvelteKit list route —\n * pages with `ORDER BY created_at DESC, <pk> ASC` (`DEFAULT_LIST_ORDER_BY`,\n * #2367). On a tenant-scoped table that page is always preceded by a\n * `tenant_id = ?` equality filter from the tenancy interceptor, so the\n * serving index leads with the tenant column and orders inside it.\n *\n * `created_at` is generator-owned on every path — a declared `createdAt`\n * field is rewritten to the same `TIMESTAMP NOT NULL DEFAULT\n * current_timestamp` column — so it is never a primary key and never\n * inline-UNIQUE. That is why only `indexes` needs checking for existing\n * coverage: no constraint-backed index can already order this column.\n *\n * Returns `null` when the table has no `created_at` column (no path emits\n * such a table today, but the helper stays total).\n */\n private getDefaultListOrderingColumns(\n columns: Record<string, { referenceKind?: string } | undefined>,\n ): string[] | null {\n if (!columns.created_at) return null;\n\n // The tenant column name is configurable (`@smrt({ tenantScoped: { field } })`),\n // so find it by reference kind rather than by the `tenant_id` spelling.\n const tenantColumn = Object.entries(columns).find(\n ([, columnDef]) => columnDef?.referenceKind === 'tenantId',\n )?.[0];\n\n return tenantColumn ? [tenantColumn, 'created_at'] : ['created_at'];\n }\n\n /**\n * Ensure the table can serve its own default list page from an index.\n *\n * Generated REST/MCP/SvelteKit list routes all page with\n * `ORDER BY created_at DESC, <pk> ASC LIMIT n` and no `created_at` index\n * existed on any schema path (the dead AST path indexed `updated_at`\n * instead), so every default list page was a sequential scan plus a top-N\n * sort of the whole table — 21 ms against 0.1 ms with the right composite on\n * the workload the assessment measured (#2363, finding A2).\n *\n * Emitted shape:\n *\n * - tenant-scoped table → `(<tenant column>, created_at)`. The tenancy\n * interceptor adds `tenant_id = ?` to every list, so the tenant column\n * leads and `created_at` orders within it.\n * - otherwise → `(created_at)`.\n *\n * No `DESC` declaration: PostgreSQL scans a B-tree backwards just as\n * cheaply, and `IndexDefinition` carries no per-column direction. The\n * trailing primary-key tiebreak is left out deliberately — its direction is\n * opposite to `created_at`'s, so no single-direction index can satisfy the\n * whole key anyway; the leading columns turn a full sort into an index scan\n * with an incremental sort over rows that share a timestamp.\n *\n * Call this AFTER {@link appendDeclaredIndexes} and BEFORE\n * {@link ensureReferenceColumnIndexes}: a declared composite that already\n * leads with the same columns suppresses this one, and this composite in\n * turn suppresses the standalone tenant index that would otherwise be a\n * redundant prefix of it.\n */\n private ensureDefaultListOrderingIndex(\n indexes: Array<{\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n jsonPath?: unknown;\n }>,\n columns: Record<string, { referenceKind?: string } | undefined>,\n tableName: string,\n ): void {\n const orderingColumns = this.getDefaultListOrderingColumns(columns);\n if (!orderingColumns) return;\n if (this.hasUnqualifiedLeadingColumns(indexes, orderingColumns)) return;\n\n const name = `${tableName}_${orderingColumns.join('_')}_idx`;\n if (indexes.some((index) => index.name === name)) return;\n\n // No `description`: ManifestIndexDefinition has no such field, and this\n // helper feeds the manifest paths as well as the structured ones.\n indexes.push({ name, columns: orderingColumns });\n }\n\n /**\n * The `(meta_type, meta_id)` owner-lookup columns for a polymorphic\n * association table, or `null` when the table isn't one.\n *\n * `SmrtPolymorphicAssociation` contributes `metaType`/`metaId`/`role` (and\n * `sortOrder`) to any concrete subclass's manifest — the scanner's\n * `FRAMEWORK_ABSTRACT_BASE_NAMES` wiring merges the abstract base's fields\n * directly into the subclass, because the base carries no `@smrt()`\n * decorator of its own to declare an index on. Detected structurally, by\n * the same three-column test `cascade.ts`'s `isPolymorphicAssociationClass`\n * uses on registry field maps: requiring all three keeps an unrelated class\n * that merely happens to carry a `metaType` column from being treated as an\n * association table.\n */\n private getPolymorphicAssociationOwnerColumns(\n columns: Record<string, unknown>,\n ): string[] | null {\n if (\n !Object.hasOwn(columns, 'meta_type') ||\n !Object.hasOwn(columns, 'meta_id') ||\n !Object.hasOwn(columns, 'role')\n ) {\n return null;\n }\n return ['meta_type', 'meta_id'];\n }\n\n /**\n * Ensure a polymorphic association table indexes its owner lookup\n * `(meta_type, meta_id)` (#2364, epic #2382 finding A3).\n *\n * A concrete association's `conflictColumns` lead with its own FK (e.g.\n * `AssetAssociation`'s `asset_id, meta_type, meta_id, role`), so\n * `meta_type`/`meta_id` sit in the *middle* of that unique index — a\n * lookup like `AssetAssociationCollection.byLeft(metaType, metaId)`\n * (\"what points at this target\") filters columns 2-3 of a 4-column index\n * and cannot use it as a leading prefix. `metaId` is deliberately a bare\n * string, never a typed `@foreignKey`/`@crossPackageRef` (there is no\n * single target table), so {@link ensureReferenceColumnIndexes} never\n * covers it either.\n *\n * Call this alongside {@link ensureDefaultListOrderingIndex}, before\n * {@link ensureReferenceColumnIndexes}.\n *\n * Blast radius: this applies retroactively to every\n * `SmrtPolymorphicAssociation` subclass across every package on the next\n * `db:migrate`, not only the ones a given change touches — roll the wave\n * out with `smrt db:migrate --postgres-safe` (#2362), same as any other\n * bulk index addition.\n */\n private ensurePolymorphicAssociationIndex(\n indexes: Array<{\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n jsonPath?: unknown;\n }>,\n columns: Record<string, unknown>,\n tableName: string,\n ): void {\n const ownerColumns = this.getPolymorphicAssociationOwnerColumns(columns);\n if (!ownerColumns) return;\n if (this.hasUnqualifiedLeadingColumns(indexes, ownerColumns)) return;\n\n const name = `${tableName}_${ownerColumns.join('_')}_idx`;\n if (indexes.some((index) => index.name === name)) return;\n\n // No `description`: ManifestIndexDefinition has no such field, and this\n // helper feeds the manifest paths as well as the structured ones.\n indexes.push({ name, columns: ownerColumns });\n }\n\n /**\n * Append `@smrt({ indexes: [...] })` declarations to a generated index set\n * (#2357).\n *\n * Shared by every schema path — build-time AST, runtime registry CTI/STI, and\n * manifest CTI/STI — so one declaration behaves identically however the schema\n * was derived. Call it *first*, ahead of\n * {@link ensureDefaultListOrderingIndex} (#2363) and\n * {@link ensureReferenceColumnIndexes}: a declared composite leading with a\n * reference column (e.g. `tenant_id`), or with the default ordering columns,\n * is exactly what those helpers' leads-with check is meant to defer to, and\n * appending afterwards would leave the table with a redundant standalone\n * index (#2384, #2359).\n *\n * Nothing is dropped quietly. A column that resolves to no column on the\n * table, a malformed entry, and a name collision with a different index are\n * all hard errors — a silently missing index is a production performance bug\n * that surfaces only under load.\n *\n * @param indexes - Generated index set, appended in place\n * @param declared - The object's `indexes` config, from any config source\n * @param columns - Emitted columns, keyed by column name\n * @param tableName - Table the indexes belong to (for error messages)\n */\n private appendDeclaredIndexes(\n indexes: Array<{\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n }>,\n declared: unknown,\n columns: Record<string, unknown>,\n tableName: string,\n ): void {\n if (declared === undefined || declared === null) return;\n if (!Array.isArray(declared)) {\n throw new Error(\n `@smrt({ indexes }) on \"${tableName}\" must be an array of index declarations.`,\n );\n }\n\n for (const entry of declared) {\n const spec = this.validateDeclaredIndex(entry, tableName);\n\n const mappedColumns = spec.columns.map((column) =>\n this.resolveDeclaredIndexColumn(column, columns),\n );\n // `Object.hasOwn`, not `in`: the column map is a plain object, so `in`\n // also answers true for `toString`, `constructor` and every other\n // `Object.prototype` key — a declaration naming one would sail past this\n // check and emit an index on a column that does not exist.\n const unknownColumns = mappedColumns.filter(\n (column) => !Object.hasOwn(columns, column),\n );\n if (unknownColumns.length > 0) {\n throw new Error(\n `Declared index \"${spec.name}\" on \"${tableName}\" references unknown column(s): ` +\n `${unknownColumns.join(', ')}. Declared index columns must name a field or ` +\n `column on the object.`,\n );\n }\n\n // No `description`: ManifestIndexDefinition has no such field, and this\n // helper feeds the manifest paths as well as the structured ones.\n const index = {\n name: spec.name,\n columns: mappedColumns,\n ...(spec.unique === true ? { unique: true } : {}),\n ...(spec.where !== undefined ? { where: spec.where } : {}),\n };\n\n const existing = indexes.find((other) => other.name === index.name);\n if (existing) {\n // An identical re-declaration is harmless (the same object can be\n // generated through more than one path). A *different* index under a\n // name already taken is not: one of the two would vanish.\n if (this.sameIndexTarget(existing, index)) continue;\n throw new Error(\n `Declared index \"${index.name}\" on \"${tableName}\" collides with a different index ` +\n `of the same name. Rename the declared index.`,\n );\n }\n\n indexes.push(index);\n }\n }\n\n /** Narrow one raw `indexes[]` entry, rejecting anything unusable. */\n private validateDeclaredIndex(\n entry: unknown,\n tableName: string,\n ): DeclaredIndexDefinition {\n const spec = entry as Partial<DeclaredIndexDefinition> | null | undefined;\n const describe = () => {\n try {\n return JSON.stringify(entry) ?? String(entry);\n } catch {\n return String(entry);\n }\n };\n\n if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {\n throw new Error(\n `Declared index on \"${tableName}\" must be an object: got ${describe()}.`,\n );\n }\n if (typeof spec.name !== 'string' || spec.name.length === 0) {\n throw new Error(\n `Declared index on \"${tableName}\" needs a non-empty \"name\": got ${describe()}.`,\n );\n }\n // A declared name is the developer's, not the generator's: shortening it\n // would silently rename what they wrote, so an over-long one is refused\n // here — at manifest generation, which `smrt dev:knowledge-check` runs —\n // rather than truncated by PostgreSQL at deploy time (#2374).\n assertIdentifierFits(spec.name, 'Declared index', `table \"${tableName}\"`);\n if (\n !Array.isArray(spec.columns) ||\n spec.columns.length === 0 ||\n !spec.columns.every(\n (column) => typeof column === 'string' && column.length > 0,\n )\n ) {\n throw new Error(\n `Declared index \"${spec.name}\" on \"${tableName}\" needs a non-empty \"columns\" array of field or column names.`,\n );\n }\n if (spec.unique !== undefined && typeof spec.unique !== 'boolean') {\n throw new Error(\n `Declared index \"${spec.name}\" on \"${tableName}\" has a non-boolean \"unique\": got ${describe()}.`,\n );\n }\n // An empty predicate would render `CREATE INDEX ... WHERE ;`, and treating\n // it as \"no predicate\" would silently widen a partial index to a full one.\n if (\n spec.where !== undefined &&\n (typeof spec.where !== 'string' || spec.where.trim().length === 0)\n ) {\n throw new Error(\n `Declared index \"${spec.name}\" on \"${tableName}\" has an empty or non-string \"where\" predicate.`,\n );\n }\n\n return spec as DeclaredIndexDefinition;\n }\n\n /**\n * Resolve a declared index column to an emitted column name.\n *\n * Declarations name SMRT fields, but only the runtime/manifest paths\n * snake_case them into columns — the build-time AST path keys columns by the\n * field name itself. Prefer an exact column hit, then the snake_case form, and\n * otherwise return the input unchanged so the caller reports it as unknown.\n */\n private resolveDeclaredIndexColumn(\n name: string,\n columns: Record<string, unknown>,\n ): string {\n // `Object.hasOwn` rather than `in` — see appendDeclaredIndexes: `in` would\n // resolve `toString` to itself and let the caller's unknown-column check\n // pass on a column that does not exist.\n if (Object.hasOwn(columns, name)) return name;\n const snakeCased = this.toSnakeCase(name);\n return Object.hasOwn(columns, snakeCased) ? snakeCased : name;\n }\n\n /** Whether two index definitions describe the same database object. */\n private sameIndexTarget(\n a: { columns: string[]; unique?: boolean; where?: string },\n b: { columns: string[]; unique?: boolean; where?: string },\n ): boolean {\n return (\n a.columns.length === b.columns.length &&\n a.columns.every((column, i) => column === b.columns[i]) &&\n (a.unique ?? false) === (b.unique ?? false) &&\n (a.where ?? null) === (b.where ?? null)\n );\n }\n\n /**\n * Ensure every reference column — `@foreignKey`, `@crossPackageRef`, and the\n * tenancy-injected `tenant_id` — has an index leading with it.\n *\n * These are the columns every relationship load, `include:` batch, reverse\n * ownership lookup and tenant-scoped read filters on. Before #2359 only the\n * registry (test) paths indexed foreign keys, the manifest (production)\n * paths indexed none of them, and `tenant_id` was indexed nowhere (#2356):\n * 196/231 `@foreignKey` and 91/92 `@crossPackageRef` columns shipped with no\n * serving index. Every schema path now calls this helper so the four paths\n * stay in step (see `schema-path-parity.test.ts`).\n *\n * \"Leading with it\" rather than \"always add\": a table that already has an\n * unqualified index starting on the column (commonly the `conflictColumns`\n * unique index, or an `indexed: true` opt-in) is already served, and a\n * standalone duplicate would only cost writes. A *partial* index does not\n * count — see {@link hasUnqualifiedLeadingIndex}.\n *\n * Reference columns are therefore always indexed; `indexed: true` on one of\n * them is redundant and harmless.\n *\n * Call this LAST in every path, after every other index (conflict, opt-in,\n * unique, the declared composites, the default list-ordering composite, and\n * any future addition) has been appended, so the leads-with suppression sees\n * the full set. In particular {@link ensureDefaultListOrderingIndex} runs\n * first on a tenant-scoped table: its `(tenant_id, created_at)` composite\n * serves the tenant equality filter too, so no standalone tenant index is\n * added (#2363).\n */\n private ensureReferenceColumnIndexes(\n indexes: Array<{\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n jsonPath?: unknown;\n }>,\n columns: Record<\n string,\n | { referenceKind?: string; primaryKey?: boolean; unique?: boolean }\n | undefined\n >,\n tableName: string,\n ): void {\n for (const [columnName, columnDef] of Object.entries(columns)) {\n const kind = columnDef?.referenceKind;\n if (\n kind !== 'foreignKey' &&\n kind !== 'crossPackageRef' &&\n kind !== 'tenantId'\n ) {\n continue;\n }\n // The primary key, and a column-level UNIQUE (rendered inline on CTI\n // tables — PostgreSQL's implicit `_key` index), are already unique\n // indexes on every engine.\n if (columnDef?.primaryKey || columnDef?.unique) continue;\n if (this.hasUnqualifiedLeadingIndex(indexes, columnName)) continue;\n\n const name = `${tableName}_${columnName}_idx`;\n if (indexes.some((index) => index.name === name)) continue;\n\n // No `description`: ManifestIndexDefinition has no such field, and this\n // helper feeds the manifest paths as well as the structured ones.\n indexes.push({ name, columns: [columnName] });\n }\n }\n\n /**\n * Whether `conflictColumns` names exactly the primary key column(s).\n *\n * `ON CONFLICT (id)` binds to the primary-key constraint on every engine, so\n * a separate unique index over the same column set is a second B-tree over\n * the same random UUIDs with nothing to add (#2359, finding A5).\n */\n private conflictColumnsArePrimaryKey(\n conflictColumns: string[],\n columns: Record<string, { primaryKey?: boolean } | undefined>,\n ): boolean {\n const primaryKeyColumns = Object.entries(columns)\n .filter(([, columnDef]) => columnDef?.primaryKey === true)\n .map(([name]) => name);\n return (\n primaryKeyColumns.length > 0 &&\n primaryKeyColumns.length === conflictColumns.length &&\n primaryKeyColumns.every((column) => conflictColumns.includes(column))\n );\n }\n\n /**\n * The table's tenant column as the generated columns describe it: the\n * column carrying `referenceKind: 'tenantId'` (set from the field's\n * `__tenancy.isTenantIdField` marker on every path).\n */\n private findTenantColumn(\n columns: Record<string, { referenceKind?: string } | undefined>,\n ): string | undefined {\n for (const [columnName, columnDef] of Object.entries(columns)) {\n if (columnDef?.referenceKind === 'tenantId') return columnName;\n }\n return undefined;\n }\n\n /**\n * Resolve the conflict target the unique conflict index must cover\n * (#2360). `config.conflictColumns` wins when the caller resolved it —\n * registry callers pass `ObjectRegistry.getConflictColumns()`, the manifest\n * pipeline passes the normalized `decoratorConfig` — otherwise the strategy\n * default, led by the tenant column when `config.tenantScoped` names a\n * tenant field that exists as a column. Both roads apply the same rule as\n * `ObjectRegistry.getConflictColumns()`, so schema and upsert agree by\n * construction (`schema-path-parity.test.ts`).\n */\n private resolveConflictTarget(\n strategy: ConflictTableStrategy,\n columns: Record<string, { referenceKind?: string } | undefined>,\n config: SchemaGeneratorConfig | undefined,\n ): { conflictColumns: string[]; tenantColumn: string | undefined } {\n const tenantColumn = this.findTenantColumn(columns);\n if (config?.conflictColumns && config.conflictColumns.length > 0) {\n return { conflictColumns: [...config.conflictColumns], tenantColumn };\n }\n const tenantScoped = config?.tenantScoped;\n const configuredTenantColumn = tenantScoped\n ? resolveTenantColumn(\n typeof tenantScoped === 'object'\n ? (tenantScoped.field ?? 'tenantId')\n : 'tenantId',\n (fieldName) => Boolean(columns[this.toSnakeCase(fieldName)]),\n (fieldName) => this.toSnakeCase(fieldName),\n )\n : undefined;\n return {\n conflictColumns: resolveConflictColumns({\n strategy,\n tenantColumn: configuredTenantColumn,\n }),\n tenantColumn,\n };\n }\n\n /**\n * Keep `(slug, context)` lookups served when `conflictColumns` are custom.\n *\n * The default conflict index is `(slug, context)`, and `loadFromSlug()`,\n * `getId()` and `getSavedId()` all filter on `slug`/`context` regardless of\n * the configured conflict key. A class that declares custom conflict\n * columns replaces that unique index and, before #2359, left those lookups\n * with no index at all — 120 tables (finding A7).\n *\n * Emitting a plain `(slug, context)` index is the safer of the two fixes:\n * routing the lookups through the conflict key would change which row a\n * slug resolves to on every such class, whereas an extra non-unique index\n * is purely additive. It is skipped when an unqualified index already\n * serves the lookup — one leading with `slug` (e.g. custom conflict columns\n * that still start with it), or the tenant-led default key\n * `(tenant_id, slug, context)` of a tenant-scoped table (#2360): every slug\n * lookup on such a table carries the tenant predicate (#2365) and is served\n * by that prefix, so a second index would only cost writes.\n */\n private ensureSlugLookupIndex(\n indexes: Array<{\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n jsonPath?: unknown;\n }>,\n columns: Record<string, { referenceKind?: string } | undefined>,\n tableName: string,\n ): void {\n if (!columns.slug || !columns.context) return;\n const tenantColumn = this.findTenantColumn(columns);\n if (\n indexes.some(\n (index) =>\n !index.where &&\n !index.jsonPath &&\n servesSlugLookup(index.columns ?? [], tenantColumn),\n )\n ) {\n return;\n }\n\n const name = `${tableName}_slug_context_idx`;\n if (indexes.some((index) => index.name === name)) return;\n\n indexes.push({ name, columns: ['slug', 'context'] });\n }\n\n /**\n * Emit the unique conflict index of an STI table from the resolved\n * conflict target (#2360): the STI default `(slug, context, _meta_type)`,\n * a custom `@smrt({ conflictColumns })` declared on the STI root, or the\n * default led by the tenant column when the root is tenant-scoped. Shared\n * by the registry and manifest STI paths so they cannot drift.\n *\n * No index is emitted when the target is the primary key itself\n * (`ON CONFLICT (id)` binds to the PK constraint), matching the CTI paths.\n */\n private emitStiConflictIndex(\n indexes: Array<{\n name: string;\n columns: string[];\n unique?: boolean;\n description?: string;\n }>,\n columns: Record<\n string,\n { referenceKind?: string; primaryKey?: boolean } | undefined\n >,\n tableName: string,\n config: SchemaGeneratorConfig | undefined,\n ): void {\n const { conflictColumns, tenantColumn } = this.resolveConflictTarget(\n 'sti',\n columns,\n config,\n );\n if (this.conflictColumnsArePrimaryKey(conflictColumns, columns)) return;\n indexes.push({\n name: conflictIndexName(tableName, conflictColumns, tenantColumn),\n columns: conflictColumns,\n unique: true,\n });\n }\n\n /**\n * Emit unique indexes for `@field({ unique: true })` columns on an STI table.\n *\n * STI columns are the union of every class in the hierarchy, all nullable,\n * so a column-level `UNIQUE` (what CTI renders inline) is not enough on its\n * own: the migration differ cannot add a column constraint to an existing\n * table, but it can add an index. Two shapes (#2359, finding A4):\n *\n * - declared on the STI **base** (and therefore inherited by every class in\n * the table): one full unique index, table-wide like CTI;\n * - declared only on a **descendant**: one partial unique index per class\n * that carries the flag (the declaring class and, through inherited field\n * metadata, its own descendants), `WHERE _meta_type = '<qualified>'`, so\n * siblings that merely share the column name are not constrained.\n * Uniqueness is then enforced per concrete class, not across the subtree\n * — a documented limitation of the discriminator-partial shape. Engines\n * without partial indexes (DuckDB, JSON) skip this shape entirely rather\n * than widen it to a table-wide UNIQUE (see DuckDBStrategy.generateIndexes\n * and SchemaComparer.compareIndexes).\n *\n * @param declarers - column name → classes whose field metadata carries\n * `unique: true` for that column, in hierarchy order (base first).\n */\n private emitStiUniqueIndexes(\n indexes: Array<{\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n }>,\n declarers: Map<string, Set<string>>,\n baseClassName: string,\n tableName: string,\n ): void {\n for (const [columnName, classNames] of declarers.entries()) {\n if (classNames.has(baseClassName)) {\n indexes.push({\n name: `${tableName}_${columnName}_unique_idx`,\n columns: [columnName],\n unique: true,\n });\n continue;\n }\n for (const className of classNames) {\n const simpleName = className.includes(':')\n ? className.slice(className.lastIndexOf(':') + 1)\n : className;\n let name = `${tableName}_${columnName}_${this.toSnakeCase(simpleName)}_unique_idx`;\n // Two classes of one hierarchy may share a simple name across\n // packages (`@a:Event`, `@b:Event`); the merged index list dedupes by\n // name, so disambiguate with a short digest of the qualified name.\n if (indexes.some((index) => index.name === name)) {\n const digest = createHash('sha256')\n .update(className)\n .digest('hex')\n .slice(0, 6);\n name = `${tableName}_${columnName}_${this.toSnakeCase(simpleName)}_${digest}_unique_idx`;\n }\n indexes.push({\n name,\n columns: [columnName],\n unique: true,\n where: `_meta_type = ${quoteStringLiteral(className)}`,\n });\n }\n }\n }\n\n /**\n * Generate index definitions\n */\n private generateIndexes(\n objectDef: SmartObjectDefinition,\n columns: Record<string, ColumnDefinition>,\n ): IndexDefinition[] {\n const indexes: IndexDefinition[] = [];\n const tableName = this.getTableName(objectDef);\n\n // Create indexes for foreign keys\n for (const [columnName, columnDef] of Object.entries(columns)) {\n if (columnDef.foreignKey) {\n indexes.push({\n name: `idx_${tableName}_${columnName}`,\n columns: [columnName],\n description: `Index for foreign key ${columnName}`,\n });\n }\n }\n\n // Create index for updated_at (common query pattern)\n indexes.push({\n name: `idx_${tableName}_updated_at`,\n columns: ['updated_at'],\n description: 'Index for timestamp queries',\n });\n\n // Create unique indexes\n for (const [columnName, columnDef] of Object.entries(columns)) {\n if (columnDef.unique && !columnDef.primaryKey) {\n indexes.push({\n name: `idx_${tableName}_${columnName}_unique`,\n columns: [columnName],\n unique: true,\n description: `Unique index for ${columnName}`,\n });\n }\n }\n\n // Declared indexes (#2357), before ensureReferenceColumnIndexes below.\n this.appendDeclaredIndexes(\n indexes,\n objectDef.decoratorConfig?.indexes,\n columns,\n tableName,\n );\n\n return indexes;\n }\n\n /**\n * Generate trigger definitions for automatic timestamp updates\n */\n private generateTriggers(\n _objectDef: SmartObjectDefinition,\n tableName: string,\n ): TriggerDefinition[] {\n return [\n {\n name: `trg_${tableName}_updated_at`,\n when: 'BEFORE',\n event: 'UPDATE',\n body: `UPDATE ${quoteIdentifier(tableName)} SET \"updated_at\" = current_timestamp WHERE \"id\" = NEW.\"id\";`,\n description: 'Automatically update updated_at timestamp',\n },\n ];\n }\n\n /**\n * Extract foreign key definitions\n */\n private extractForeignKeys(\n columns: Record<string, ColumnDefinition>,\n ): ForeignKeyDefinition[] {\n const foreignKeys: ForeignKeyDefinition[] = [];\n\n for (const [columnName, columnDef] of Object.entries(columns)) {\n if (columnDef.foreignKey) {\n foreignKeys.push({\n column: columnName,\n referencesTable: columnDef.foreignKey.table,\n referencesColumn: columnDef.foreignKey.column,\n onDelete: columnDef.foreignKey.onDelete,\n onUpdate: columnDef.foreignKey.onUpdate,\n });\n }\n }\n\n return foreignKeys;\n }\n\n /**\n * Extract schema dependencies from foreign keys and inheritance\n */\n private extractDependencies(\n objectDef: SmartObjectDefinition,\n foreignKeys: ForeignKeyDefinition[],\n ): string[] {\n const dependencies = new Set<string>();\n\n // Add dependencies from foreign keys\n for (const fk of foreignKeys) {\n dependencies.add(fk.referencesTable);\n }\n\n // Add dependencies from base class\n if (\n objectDef.extends &&\n objectDef.extends !== 'SmrtObject' &&\n objectDef.extends !== 'SmrtCollection'\n ) {\n dependencies.add(this.classNameToTableName(objectDef.extends));\n }\n\n return Array.from(dependencies);\n }\n\n /**\n * Generate version hash for schema\n */\n private generateVersion(objectDef: SmartObjectDefinition): string {\n const content = JSON.stringify({\n className: objectDef.className,\n fields: objectDef.fields,\n extends: objectDef.extends,\n });\n return createHash('sha256').update(content).digest('hex').substring(0, 8);\n }\n\n /**\n * Get table name from object definition\n */\n private getTableName(objectDef: SmartObjectDefinition): string {\n return this.classNameToTableName(objectDef.className);\n }\n\n /**\n * Convert class name to table name (camelCase to snake_case, pluralized)\n */\n private classNameToTableName(className: string): string {\n return classnameToTablename(className);\n }\n\n /**\n * Extract package name from file path\n */\n private extractPackageName(filePath: string): string {\n const match = filePath.match(/packages\\/([^/]+)/);\n return match ? match[1] : 'unknown';\n }\n\n /**\n * Generate schema definition from ObjectRegistry fields (runtime)\n *\n * This method builds a SchemaDefinition from ObjectRegistry cached fields,\n * enabling schema generation from decorated classes at runtime.\n *\n * @param className - Class name to look up in ObjectRegistry\n * @param tableName - Table name (from SMRT_TABLE_NAME or derived)\n * @param fields - Map of Field definitions from ObjectRegistry\n * @returns Schema definition object\n */\n generateSchemaFromRegistry(\n className: string,\n tableName: string,\n fields: Map<string, RegistryField>,\n config?: SchemaGeneratorConfig,\n ): SchemaDefinition {\n const columns: Record<string, ColumnDefinition> = {};\n\n // Check for custom primary key\n let hasCustomPK = false;\n for (const [_fieldName, field] of fields.entries()) {\n if (field._meta?.primaryKey) {\n hasCustomPK = true;\n break;\n }\n }\n\n // Add default SMRT fields if no custom primary key\n if (!hasCustomPK) {\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n description: 'Primary identifier',\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n description: 'URL-friendly identifier',\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n defaultValue: '',\n description: 'Context for slug scoping',\n };\n }\n\n // Track timestamp fields to avoid duplicates\n let hasCreatedAt = false;\n let hasUpdatedAt = false;\n // Track regular (column-backed) fields that opted into a plain column index\n // via `indexed: true`. FK, unique, and meta fields each go through their\n // own dedicated index emission paths and are excluded from this set.\n const indexedColumns = new Set<string>();\n\n // Add fields from ObjectRegistry\n for (const [fieldName, field] of fields.entries()) {\n // Skip transient fields (non-persisted)\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n const isDefaultSmrtColumn =\n fieldName === 'id' || fieldName === 'slug' || fieldName === 'context';\n\n // Default SMRT columns are added above when we use the standard primary\n // key path. When a class declares a custom primary key, only explicitly\n // declared id/slug/context fields should survive this loop.\n if (\n isDefaultSmrtColumn &&\n (!hasCustomPK || field._meta?.__smrtSystemField === true)\n ) {\n continue;\n }\n\n // Track timestamp fields\n if (fieldName === 'created_at' || fieldName === 'createdAt') {\n if (hasCreatedAt) continue;\n hasCreatedAt = true;\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n continue;\n }\n if (fieldName === 'updated_at' || fieldName === 'updatedAt') {\n if (hasUpdatedAt) continue;\n hasUpdatedAt = true;\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n continue;\n }\n\n // Skip relationship fields that don't create columns\n // oneToMany and manyToMany are relationship metadata, not actual database columns\n // foreignKey DOES create a column (it stores the foreign key ID)\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column (STI only)\n // In CTI mode, meta fields shouldn't be used, but skip them anyway for safety\n if (field.type === 'meta') {\n continue;\n }\n\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n // If _meta.nullable is true, the field can be null regardless of required\n notNull: field._meta?.nullable ? false : field._meta?.required || false,\n primaryKey: field._meta?.primaryKey || false,\n unique: field._meta?.unique || false,\n description: field._meta?.description,\n };\n\n // Get default value\n if (\n field._meta?.default !== undefined &&\n this.shouldEmitDefault(field, field._meta.default)\n ) {\n columnDef.defaultValue = field._meta.default;\n }\n // Note: Removed automatic NOT NULL DEFAULT '' for TEXT columns\n // This was forcing all TEXT fields to be NOT NULL regardless of required option\n // If DuckDB ANY type inference is an issue, it should be handled at the adapter level\n // or with an explicit field option, not by silently changing schema semantics\n\n // Handle foreign keys\n if (field.type === 'foreignKey') {\n // Type cast to access relationship-specific properties\n const relatedName = field.related; // Top-level property\n const onDeleteAction = field._meta?.onDelete;\n\n if (relatedName) {\n columnDef.foreignKey = {\n table: this.classNameToTableName(relatedName),\n column: 'id',\n onDelete: onDeleteAction || 'CASCADE',\n onUpdate: 'CASCADE',\n };\n }\n }\n\n // Track opt-in column indexes for regular (non-FK, non-unique) fields.\n // FK columns already get auto-indexed below; unique columns produce\n // their own unique index; meta fields go through the jsonPath path.\n const isIndexed = field._meta?.indexed === true || field.indexed === true;\n if (\n isIndexed &&\n !columnDef.foreignKey &&\n !columnDef.unique &&\n !columnDef.primaryKey\n ) {\n indexedColumns.add(this.toSnakeCase(fieldName));\n }\n\n columns[this.toSnakeCase(fieldName)] = columnDef;\n }\n\n // Ensure timestamp columns exist\n if (!hasCreatedAt) {\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n }\n if (!hasUpdatedAt) {\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n }\n\n this.reconcileRegistryForeignKeyColumnTypes(\n columns,\n fields,\n config?.registry,\n );\n\n // Generate indexes. No `<table>_id_idx`: the primary key is already a\n // unique index on every engine, and the second B-tree over the same\n // random UUIDs cost writes on 238/238 tables for nothing (#2359, A5).\n // The differ drops the legacy one from existing databases.\n const indexes: IndexDefinition[] = [];\n\n if (!hasCustomPK) {\n // Tenant-scoped tables key on `(tenant_id, slug, context)` under the\n // stable `<table>_slug_context_idx` name (#2360).\n const { conflictColumns, tenantColumn } = this.resolveConflictTarget(\n 'cti',\n columns,\n config,\n );\n if (!this.conflictColumnsArePrimaryKey(conflictColumns, columns)) {\n indexes.push({\n name: conflictIndexName(tableName, conflictColumns, tenantColumn),\n columns: conflictColumns,\n unique: true,\n description: `Unique conflict index for ${className}`,\n });\n }\n }\n\n // Custom conflict columns replace the (slug, context) index, but slug\n // loading still filters on it (#2359, A7).\n this.ensureSlugLookupIndex(indexes, columns, tableName);\n\n // Emit opt-in column indexes for regular fields tagged with `indexed: true`\n for (const colName of indexedColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n description: `Index for ${colName}`,\n });\n }\n\n // Indexes declared with `@smrt({ indexes: [...] })` (#2357). Appended\n // before the reference-column helper so a declared composite leading with\n // a reference column suppresses the standalone auto index.\n this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);\n\n // The default list page orders by created_at (#2363) — after the declared\n // indexes, before the reference-column pass.\n this.ensureDefaultListOrderingIndex(indexes, columns, tableName);\n\n // Polymorphic association owner lookup `(meta_type, meta_id)` (#2364,\n // epic #2382 finding A3) — alongside the default-ordering composite,\n // before the reference-column pass.\n this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);\n\n // Reference columns (@foreignKey / @crossPackageRef / tenant_id) are\n // always indexed (#2356, #2359).\n this.ensureReferenceColumnIndexes(indexes, columns, tableName);\n\n // Last: nothing may lengthen a name after this (#2374).\n enforceIdentifierLimits(tableName, indexes);\n\n return {\n tableName,\n columns,\n indexes,\n triggers: [],\n foreignKeys: this.extractForeignKeys(columns),\n dependencies: [],\n version: createHash('sha256')\n .update(JSON.stringify(columns))\n .digest('hex')\n .substring(0, 8),\n packageName: 'runtime',\n baseClass: 'SmrtObject',\n };\n }\n\n /**\n * Generate STI (Single Table Inheritance) schema from ObjectRegistry fields\n *\n * Creates a shared table for an inheritance hierarchy with:\n * - _meta_type: Discriminator column to identify class type\n * - _meta_data: JSON column for flexible field storage\n * - Union of all FK columns from descendants (all nullable)\n * - One plain index per reference column (FK / cross-package ref /\n * tenant_id). Plain rather than partial-by-class: base-class polymorphic\n * queries carry no `_meta_type` predicate, so a partial index could not\n * serve them, while a plain index serves both those and the child\n * collections' `_meta_type = X AND fk = ?` filters — one index per column\n * instead of one per (column, class) (#2359).\n * - Unique indexes for `@field({ unique: true })` columns — see\n * {@link emitStiUniqueIndexes}.\n *\n * @param baseClassName - Base class name for the STI hierarchy\n * @param tableName - Shared table name (from base class)\n * @param fields - Map of Field definitions from base class\n * @returns Schema definition object for STI table\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = '';\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * roomId = foreignKey(Room);\n * }\n *\n * // Generates single table 'events' with:\n * // - title (from Event)\n * // - room_id (from Meeting, nullable)\n * // - _meta_type TEXT NOT NULL (discriminator)\n * // - _meta_data JSON (flexible storage)\n * ```\n */\n async generateSTISchemaFromRegistry(\n baseClassName: string,\n tableName: string,\n _fields: Map<string, RegistryField>,\n config?: SchemaGeneratorConfig,\n ): Promise<SchemaDefinition> {\n const ObjectRegistry =\n config?.registry ?? (await import('../registry.js')).ObjectRegistry;\n const columns: Record<string, ColumnDefinition> = {};\n\n // Add default SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n description: 'Primary identifier',\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n description: 'URL-friendly identifier',\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n defaultValue: '',\n description: 'Context for slug scoping',\n };\n\n // Add STI discriminator column\n columns._meta_type = {\n type: 'TEXT',\n notNull: true,\n description: 'Class type discriminator for STI',\n };\n\n // Add meta column for flexible storage\n columns._meta_data = {\n type: 'JSON',\n notNull: false,\n description: 'Flexible JSON storage for meta() fields',\n };\n\n // Track timestamp fields to avoid duplicates\n let hasCreatedAt = false;\n let hasUpdatedAt = false;\n\n // Get all descendants to aggregate fields\n const descendants = ObjectRegistry.getDescendants(baseClassName);\n const allClassNames = [baseClassName, ...descendants];\n\n // Track meta fields opted into JSON-path indexing (deduped across STI subtypes)\n const indexedMetaFields = new Set<string>();\n // Track regular-field columns opted into plain column indexing\n const indexedStiColumns = new Set<string>();\n // Track `unique: true` columns and the classes declaring them (#2359, A4)\n const uniqueColumnDeclarers = new Map<string, Set<string>>();\n\n // Aggregate fields from base and all descendants\n for (const className of allClassNames) {\n const classFields: Map<string, RegistryField> =\n await ObjectRegistry.getAllFields(className);\n\n for (const [fieldName, field] of classFields.entries()) {\n // Skip transient fields\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n // Capture indexed meta fields before the skip-meta branch below\n if (\n field.type === 'meta' &&\n (field.indexed === true || field._meta?.indexed === true)\n ) {\n indexedMetaFields.add(fieldName);\n }\n\n // Skip default fields already added\n if (\n fieldName === 'id' ||\n fieldName === 'slug' ||\n fieldName === 'context'\n ) {\n continue;\n }\n\n // Track timestamp fields\n if (fieldName === 'created_at' || fieldName === 'createdAt') {\n if (hasCreatedAt) continue;\n hasCreatedAt = true;\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n continue;\n }\n if (fieldName === 'updated_at' || fieldName === 'updatedAt') {\n if (hasUpdatedAt) continue;\n hasUpdatedAt = true;\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n continue;\n }\n\n // Skip relationship fields that don't create columns\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column\n if (field.type === 'meta') {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n\n // `unique: true` is recorded per declaring class (before the\n // inherited-column skip below, since descendants re-list inherited\n // fields) so the index shape can depend on where it was declared.\n if (field._meta?.unique === true) {\n let declarers = uniqueColumnDeclarers.get(columnName);\n if (!declarers) {\n declarers = new Set();\n uniqueColumnDeclarers.set(columnName, declarers);\n }\n declarers.add(className);\n }\n\n // Check if column already exists (inherited from parent)\n if (columns[columnName]) {\n continue;\n }\n\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n // STI: All columns nullable (union of child fields)\n notNull: false,\n primaryKey: false,\n // Uniqueness is enforced through indexes (emitStiUniqueIndexes), not\n // an inline column constraint the differ could never add later.\n unique: false,\n description: field._meta?.description,\n };\n\n // Get default value (but not applied in STI - defaults handled by application)\n if (\n field._meta?.default !== undefined &&\n this.shouldEmitDefault(field, field._meta.default)\n ) {\n columnDef.defaultValue = field._meta.default;\n }\n\n // Handle foreign keys\n if (field.type === 'foreignKey') {\n const relatedName = field.related; // Top-level property\n const onDeleteAction = field._meta?.onDelete;\n\n if (relatedName) {\n columnDef.foreignKey = {\n table: this.classNameToTableName(relatedName),\n column: 'id',\n onDelete: onDeleteAction || 'CASCADE',\n onUpdate: 'CASCADE',\n };\n }\n }\n\n // Track opt-in column indexes for regular STI columns\n const isIndexed =\n field._meta?.indexed === true || field.indexed === true;\n if (isIndexed && !columnDef.foreignKey) {\n indexedStiColumns.add(columnName);\n }\n\n columns[columnName] = columnDef;\n }\n }\n\n // Ensure timestamp columns exist\n if (!hasCreatedAt) {\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Creation timestamp',\n };\n }\n if (!hasUpdatedAt) {\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n defaultValue: 'current_timestamp',\n description: 'Last update timestamp',\n };\n }\n\n for (const className of allClassNames) {\n const classFields = await ObjectRegistry.getAllFields(className);\n this.reconcileRegistryForeignKeyColumnTypes(\n columns,\n classFields,\n ObjectRegistry,\n );\n }\n\n // Generate indexes. No `<table>_id_idx` — see generateSchemaFromRegistry.\n const indexes: IndexDefinition[] = [];\n\n // Unique conflict index — by default (slug, context, _meta_type): STI\n // subclasses share a table, so the discriminator participates in\n // identity and two subtypes can share the same (slug, context). The key\n // is derived from the resolved config rather than hard-coded (#2360,\n // A4): a custom `@smrt({ conflictColumns })` on the STI root replaces it\n // (previously ignored here while `getConflictColumns()` honoured it, so\n // every save failed on PostgreSQL), and a tenant-scoped root leads it\n // with the tenant column. PostgreSQL UPSERT requires the live schema to\n // carry a matching unique index; the migration differ repairs older\n // deployments in place by name (issue #1165, #2360).\n this.emitStiConflictIndex(indexes, columns, tableName, config);\n\n // Index on type column (for polymorphic queries)\n indexes.push({\n name: `${tableName}_meta_type_idx`,\n columns: ['_meta_type'],\n description: 'Index for type discriminator queries',\n });\n\n // Unique indexes for `@field({ unique: true })` columns (#2359, A4)\n this.emitStiUniqueIndexes(\n indexes,\n uniqueColumnDeclarers,\n baseClassName,\n tableName,\n );\n\n // Custom conflict columns replace the (slug, context, _meta_type) index,\n // but slug loading still filters on slug/context (#2359, A7).\n this.ensureSlugLookupIndex(indexes, columns, tableName);\n\n // JSON-path indexes for @meta({ indexed: true }) fields\n for (const fieldName of indexedMetaFields) {\n indexes.push({\n name: `${tableName}_meta_${this.toSnakeCase(fieldName)}_idx`,\n columns: [],\n jsonPath: { column: '_meta_data', path: fieldName },\n description: `JSON-path index for @meta({ indexed: true }) field ${fieldName}`,\n });\n }\n\n // Plain column indexes for regular STI columns opted in via `indexed: true`\n for (const colName of indexedStiColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n description: `Index for ${colName}`,\n });\n }\n\n // Indexes declared on the STI base with `@smrt({ indexes: [...] })`\n // (#2357). Descendants share one table, so the base owns its access paths\n // and a declaration there applies to every subtype's reads. Appended\n // before the reference-column helper so a declared composite leading with\n // a reference column suppresses the standalone auto index.\n this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);\n\n // The default list page orders by created_at (#2363) — after the declared\n // indexes, before the reference-column pass.\n this.ensureDefaultListOrderingIndex(indexes, columns, tableName);\n\n // Polymorphic association owner lookup `(meta_type, meta_id)` (#2364,\n // epic #2382 finding A3) — alongside the default-ordering composite,\n // before the reference-column pass.\n this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);\n\n // Reference columns (@foreignKey / @crossPackageRef / tenant_id) are\n // always indexed (#2356, #2359).\n this.ensureReferenceColumnIndexes(indexes, columns, tableName);\n\n // Last: nothing may lengthen a name after this (#2374).\n enforceIdentifierLimits(tableName, indexes);\n\n return {\n tableName,\n columns,\n indexes,\n triggers: [],\n foreignKeys: this.extractForeignKeys(columns),\n dependencies: [],\n version: createHash('sha256')\n .update(JSON.stringify({ columns, baseClassName, descendants }))\n .digest('hex')\n .substring(0, 8),\n packageName: 'runtime',\n baseClass: 'SmrtObject',\n };\n }\n\n /**\n * Generate STI schema from manifest data (build-time, no runtime registry)\n *\n * This method generates STI schema purely from manifest data, without depending\n * on ObjectRegistry. This enables pre-generating schemas at build time for\n * efficient external package consumption.\n *\n * @param baseClassName - Base class name for the STI hierarchy\n * @param tableName - Shared table name (from base class)\n * @param baseFields - Fields from the base class\n * @param manifest - Full manifest containing all objects\n * @returns ManifestSchema for storage in manifest.json\n */\n generateSTISchemaFromManifest(\n baseClassName: string,\n tableName: string,\n _baseFields: Record<string, FieldDefinition>,\n manifest: SmartObjectManifest,\n config?: SchemaGeneratorConfig,\n ): ManifestSchema {\n const columns: Record<string, ManifestColumnDefinition> = {};\n\n // Add default SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n default: '',\n };\n\n // Add STI discriminator column\n columns._meta_type = {\n type: 'TEXT',\n notNull: true,\n };\n\n // Add meta column for flexible storage\n columns._meta_data = {\n type: 'JSON',\n notNull: false,\n };\n\n // Timestamp fields\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n // Find all descendants in manifest. Manifest keys are qualified names\n // while callers may pass the base's simple name (an external STI base is\n // reported that way); resolve to the key so the base's own fields are\n // aggregated and the base-declared unique check below sees the same\n // spelling the descendant loop records.\n const baseKey = this.resolveManifestClassKey(baseClassName, manifest);\n const descendants = this.findDescendantsInManifest(baseKey, manifest);\n const allClassNames = [baseKey, ...descendants];\n\n // Track meta fields opted into JSON-path indexing (deduped across STI subtypes)\n const indexedMetaFields = new Set<string>();\n // Track regular columns opted into plain column indexing\n const indexedStiColumns = new Set<string>();\n // Track `unique: true` columns and the classes declaring them (#2359, A4)\n const uniqueColumnDeclarers = new Map<string, Set<string>>();\n\n // Aggregate fields from base and all descendants\n for (const className of allClassNames) {\n const objDef = manifest.objects[className];\n if (!objDef) continue;\n\n for (const [fieldName, field] of Object.entries(objDef.fields)) {\n // Skip transient fields\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n // Capture indexed meta fields before the skip-meta branch below.\n // `indexed` is normally carried in `_meta`, but tolerate a legacy\n // top-level flag too — RegistryField models both placements.\n const indexedField: RegistryField = field;\n if (\n field.type === 'meta' &&\n (indexedField.indexed === true || field._meta?.indexed === true)\n ) {\n indexedMetaFields.add(fieldName);\n }\n\n // Skip default fields already added\n if (\n fieldName === 'id' ||\n fieldName === 'slug' ||\n fieldName === 'context' ||\n fieldName === 'created_at' ||\n fieldName === 'createdAt' ||\n fieldName === 'updated_at' ||\n fieldName === 'updatedAt'\n ) {\n continue;\n }\n\n // Skip relationship fields that don't create columns\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields - they're stored in _meta_data JSONB column\n if (field.type === 'meta') {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n\n // `unique: true` per declaring class — see generateSTISchemaFromRegistry.\n if (field._meta?.unique === true) {\n let declarers = uniqueColumnDeclarers.get(columnName);\n if (!declarers) {\n declarers = new Set();\n uniqueColumnDeclarers.set(columnName, declarers);\n }\n declarers.add(className);\n }\n\n // Check if column already exists (inherited from parent)\n if (columns[columnName]) {\n continue;\n }\n\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ManifestColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n // STI: All columns nullable (union of child fields)\n notNull: false,\n };\n\n // Get default value\n if (\n field.default !== undefined &&\n this.shouldEmitDefault(field, field.default)\n ) {\n columnDef.default = field.default;\n }\n\n // Track opt-in column indexes for regular STI columns\n const isIndexed =\n indexedField.indexed === true || field._meta?.indexed === true;\n if (isIndexed && field.type !== 'foreignKey') {\n indexedStiColumns.add(columnName);\n }\n\n columns[columnName] = columnDef;\n }\n }\n\n // Generate indexes. No `<table>_id_idx` — see generateSchemaFromRegistry.\n const indexes: ManifestIndexDefinition[] = [];\n\n // Unique conflict index (default slug, context, _meta_type; custom or\n // tenant-led per the root's config) — see generateSTISchemaFromRegistry.\n this.emitStiConflictIndex(indexes, columns, tableName, config);\n\n // Index on type column (for polymorphic queries)\n indexes.push({\n name: `${tableName}_meta_type_idx`,\n columns: ['_meta_type'],\n });\n\n // Unique indexes for `@field({ unique: true })` columns (#2359, A4)\n this.emitStiUniqueIndexes(\n indexes,\n uniqueColumnDeclarers,\n baseKey,\n tableName,\n );\n\n // Custom conflict columns replace the (slug, context, _meta_type) index,\n // but slug loading still filters on slug/context (#2359, A7).\n this.ensureSlugLookupIndex(indexes, columns, tableName);\n\n // JSON-path indexes for @meta({ indexed: true }) fields\n for (const fieldName of indexedMetaFields) {\n indexes.push({\n name: `${tableName}_meta_${this.toSnakeCase(fieldName)}_idx`,\n columns: [],\n jsonPath: { column: '_meta_data', path: fieldName },\n });\n }\n\n // Plain column indexes for regular STI columns opted in via `indexed: true`\n for (const colName of indexedStiColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n });\n }\n\n // Indexes declared on the STI base with `@smrt({ indexes: [...] })`\n // (#2357), before the reference-column helper so a declared composite\n // leading with a reference column suppresses the standalone auto index.\n this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);\n\n // The default list page orders by created_at (#2363) — after the declared\n // indexes, before the reference-column pass.\n this.ensureDefaultListOrderingIndex(indexes, columns, tableName);\n\n // Polymorphic association owner lookup `(meta_type, meta_id)` (#2364,\n // epic #2382 finding A3) — alongside the default-ordering composite,\n // before the reference-column pass.\n this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);\n\n // Reference columns (@foreignKey / @crossPackageRef / tenant_id) are\n // always indexed (#2356, #2359). Runs last so every consumer of the\n // structured `indexes` array (migrations, test databases, aggregation)\n // creates them; the cached `ddl` string below is CREATE TABLE only and is\n // not an executable representation of the table (#2358).\n this.ensureReferenceColumnIndexes(indexes, columns, tableName);\n\n // Last: nothing may lengthen a name after this (#2374).\n enforceIdentifierLimits(tableName, indexes);\n\n const schemaDefinition: SchemaDefinition = {\n tableName,\n columns: this.convertManifestColumnsToSchemaColumns(columns),\n indexes: indexes.map((idx) => ({\n name: idx.name,\n columns: idx.columns,\n unique: idx.unique,\n where: idx.where,\n jsonPath: idx.jsonPath,\n })),\n triggers: [],\n foreignKeys: [],\n dependencies: [],\n version: '',\n packageName: '',\n };\n\n const ddl = this.generateSQL(schemaDefinition);\n\n // Generate version hash\n const version = createHash('sha256')\n .update(JSON.stringify({ columns, baseClassName, descendants }))\n .digest('hex')\n .substring(0, 8);\n\n return {\n tableName,\n ddl,\n columns,\n indexes,\n version,\n };\n }\n\n /**\n * Generate CTI (Class Table Inheritance) schema from manifest data\n *\n * @param className - Class name\n * @param tableName - Table name\n * @param fields - Fields from the class\n * @returns ManifestSchema for storage in manifest.json\n */\n generateCTISchemaFromManifest(\n className: string,\n tableName: string,\n fields: Record<string, FieldDefinition>,\n config?: SchemaGeneratorConfig,\n ): ManifestSchema {\n const columns: Record<string, ManifestColumnDefinition> = {};\n\n // Track regular columns opted into a plain index via `@field({ indexed: true })`.\n // Mirrors the STI path (generateSTISchemaFromManifest) so CTI tables honor\n // the documented `FieldOptions.indexed` flag too.\n const indexedColumns = new Set<string>();\n\n // Add default SMRT fields\n columns.id = {\n type: this.getIdColumnType(config),\n primaryKey: true,\n referenceKind: 'id',\n notNull: true,\n };\n\n columns.slug = {\n type: 'TEXT',\n notNull: true,\n };\n\n columns.context = {\n type: 'TEXT',\n notNull: true,\n default: '',\n };\n\n // Timestamp fields\n columns.created_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n columns.updated_at = {\n type: 'TIMESTAMP',\n notNull: true,\n default: 'current_timestamp',\n };\n\n // Add class fields\n for (const [fieldName, field] of Object.entries(fields)) {\n // Skip transient fields\n if (field.transient || field._meta?.transient) {\n continue;\n }\n\n // Skip default fields already added\n if (\n fieldName === 'id' ||\n fieldName === 'slug' ||\n fieldName === 'context' ||\n fieldName === 'created_at' ||\n fieldName === 'createdAt' ||\n fieldName === 'updated_at' ||\n fieldName === 'updatedAt'\n ) {\n continue;\n }\n\n // Skip relationship fields that don't create columns\n if (field.type === 'oneToMany' || field.type === 'manyToMany') {\n continue;\n }\n\n // Skip meta fields\n if (field.type === 'meta') {\n continue;\n }\n\n const columnName = this.toSnakeCase(fieldName);\n const sqlType = this.getRelationshipColumnType(field);\n\n const columnDef: ManifestColumnDefinition = {\n type: sqlType,\n referenceKind: this.getReferenceKind(field),\n notNull: field._meta?.nullable ? false : field.required || false,\n unique: field._meta?.unique || false,\n };\n\n if (\n field.default !== undefined &&\n this.shouldEmitDefault(field, field.default)\n ) {\n columnDef.default = field.default;\n }\n\n // Opt-in plain column index. FK columns and unique columns get their own\n // indexes, so skip those here.\n const indexedField: RegistryField = field;\n const isIndexed =\n indexedField.indexed === true || field._meta?.indexed === true;\n if (isIndexed && field.type !== 'foreignKey' && !columnDef.unique) {\n indexedColumns.add(columnName);\n }\n\n columns[columnName] = columnDef;\n }\n\n // Generate indexes. No `<table>_id_idx` — see generateSchemaFromRegistry.\n const indexes: ManifestIndexDefinition[] = [];\n\n // Custom conflict columns if provided, otherwise slug+context — led by\n // the tenant column on a tenant-scoped table, under the same stable\n // index name (#2360).\n const { conflictColumns, tenantColumn } = this.resolveConflictTarget(\n 'cti',\n columns,\n config,\n );\n if (!this.conflictColumnsArePrimaryKey(conflictColumns, columns)) {\n indexes.push({\n name: conflictIndexName(tableName, conflictColumns, tenantColumn),\n columns: conflictColumns,\n unique: true,\n });\n }\n\n // Custom conflict columns replace the (slug, context) index, but slug\n // loading still filters on it (#2359, A7).\n this.ensureSlugLookupIndex(indexes, columns, tableName);\n\n // Plain column indexes for regular columns opted in via `indexed: true`.\n for (const colName of indexedColumns) {\n indexes.push({\n name: `${tableName}_${colName}_idx`,\n columns: [colName],\n });\n }\n\n // Indexes declared with `@smrt({ indexes: [...] })` (#2357), before the\n // reference-column helper so a declared composite leading with a\n // reference column suppresses the standalone auto index.\n this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);\n\n // The default list page orders by created_at (#2363) — after the declared\n // indexes, before the reference-column pass.\n this.ensureDefaultListOrderingIndex(indexes, columns, tableName);\n\n // Polymorphic association owner lookup `(meta_type, meta_id)` (#2364,\n // epic #2382 finding A3) — alongside the default-ordering composite,\n // before the reference-column pass.\n this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);\n\n // Reference columns (@foreignKey / @crossPackageRef / tenant_id) are\n // always indexed (#2356, #2359). Runs last so every consumer of the\n // structured `indexes` array (migrations, test databases, aggregation)\n // creates them; the cached `ddl` string below is CREATE TABLE only and is\n // not an executable representation of the table (#2358).\n this.ensureReferenceColumnIndexes(indexes, columns, tableName);\n\n // Last: nothing may lengthen a name after this (#2374).\n enforceIdentifierLimits(tableName, indexes);\n\n const schemaDefinition: SchemaDefinition = {\n tableName,\n columns: this.convertManifestColumnsToSchemaColumns(columns),\n indexes: indexes.map((idx) => ({\n name: idx.name,\n columns: idx.columns,\n unique: idx.unique,\n where: idx.where,\n jsonPath: idx.jsonPath,\n })),\n triggers: [],\n foreignKeys: [],\n dependencies: [],\n version: '',\n packageName: '',\n };\n\n const ddl = this.generateSQL(schemaDefinition);\n\n // Generate version hash\n const version = createHash('sha256')\n .update(JSON.stringify({ columns, className }))\n .digest('hex')\n .substring(0, 8);\n\n return {\n tableName,\n ddl,\n columns,\n indexes,\n version,\n };\n }\n\n /**\n * Resolve a class name (simple or qualified) to the key it is stored under\n * in `manifest.objects`. Falls back to the input when the manifest does not\n * carry the class (e.g. an external STI base absent from the aggregated\n * manifest), so callers can keep using it as an opaque label.\n */\n private resolveManifestClassKey(\n className: string,\n manifest: SmartObjectManifest,\n ): string {\n if (manifest.objects[className]) return className;\n const simpleName = className.includes(':')\n ? className.slice(className.lastIndexOf(':') + 1)\n : className;\n for (const [key, obj] of Object.entries(manifest.objects)) {\n if (obj.qualifiedName === className || obj.className === simpleName) {\n return key;\n }\n }\n return className;\n }\n\n /**\n * Find all descendants of a class in the manifest\n *\n * Note: Manifest keys are now qualified names (@pkg:ClassName) but `extends` field\n * stores simple PascalCase class names. We need to handle both qualified and simple\n * name lookups.\n *\n * Issue #713: Updated to handle qualified names in manifest keys\n */\n private findDescendantsInManifest(\n baseClassName: string,\n manifest: SmartObjectManifest,\n visited: Set<string> = new Set(),\n ): string[] {\n const descendants: string[] = [];\n\n // Prevent infinite recursion (e.g., class extends same-named class from another package)\n if (visited.has(baseClassName)) return descendants;\n visited.add(baseClassName);\n\n // Extract the simple class name from qualified name if needed\n // e.g., '@happyvertical/smrt-core:PolyEvent' -> 'PolyEvent'\n const simpleBaseClassName = baseClassName.includes(':')\n ? baseClassName.split(':').pop() || baseClassName\n : baseClassName;\n const baseClassLower = simpleBaseClassName.toLowerCase();\n\n for (const [name, obj] of Object.entries(manifest.objects)) {\n // Skip self-references (class extending same-named class from another package)\n if (\n obj.className.toLowerCase() === baseClassLower &&\n obj.extends?.toLowerCase() === baseClassLower\n ) {\n continue;\n }\n // obj.extends is a simple class name (e.g., 'PolyEvent')\n // Compare with the simple (non-qualified) version of the base class name\n if (obj.extends?.toLowerCase() === baseClassLower) {\n descendants.push(name);\n // Recursively find descendants of this class\n // Pass the qualified name (manifest key) for recursive lookup\n descendants.push(\n ...this.findDescendantsInManifest(name, manifest, visited),\n );\n }\n }\n\n return descendants;\n }\n\n /**\n * Convert ManifestColumnDefinition to ColumnDefinition\n */\n private convertManifestColumnsToSchemaColumns(\n manifestColumns: Record<string, ManifestColumnDefinition>,\n ): Record<string, ColumnDefinition> {\n const columns: Record<string, ColumnDefinition> = {};\n\n for (const [name, col] of Object.entries(manifestColumns)) {\n columns[name] = {\n type: col.type as SQLDataType,\n primaryKey: col.primaryKey,\n referenceKind: col.referenceKind,\n notNull: col.notNull,\n unique: col.unique,\n defaultValue: col.default,\n };\n }\n\n return columns;\n }\n\n /**\n * Convert camelCase to snake_case\n */\n private toSnakeCase(str: string): string {\n return str\n .replace(/([A-Z])/g, '_$1')\n .toLowerCase()\n .replace(/^_/, '');\n }\n\n /**\n * Generate the CREATE TABLE statement for a schema definition.\n *\n * With an `engine` this delegates to that engine's DDL strategy. Without\n * one it renders the engine-neutral preview stored in `schema.ddl` and\n * `manifest.json`: abstract SQL types, no indexes, no triggers. That\n * preview is NOT an executable representation of the table (#2358) —\n * executable paths (`db:migrate`, `MigrationGenerator`, `SchemaAggregator`,\n * `createIsolatedTestDbFromManifest`) render `schema.columns` and\n * `schema.indexes` through `getDDLStrategy(engine)` instead.\n *\n * @param schema - Schema definition object\n * @param engine - Optional target engine; omit for the neutral preview\n * @returns SQL CREATE TABLE statement (no indexes)\n */\n generateSQL(schema: SchemaDefinition, engine?: DatabaseEngine): string {\n if (engine) {\n return getDDLStrategy(engine).generateCreateTable(schema);\n }\n\n const { tableName, columns } = schema;\n let sql = `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(tableName)} (\\n`;\n\n for (const [columnName, columnDef] of Object.entries(columns)) {\n const parts = [` ${quoteIdentifier(columnName)} ${columnDef.type}`];\n\n if (columnDef.primaryKey) {\n parts.push('PRIMARY KEY');\n }\n\n if (columnDef.notNull) {\n parts.push('NOT NULL');\n }\n\n if (columnDef.unique && !columnDef.primaryKey) {\n parts.push('UNIQUE');\n }\n\n if (columnDef.defaultValue !== undefined) {\n parts.push(\n `DEFAULT ${this.formatDefaultValue(\n columnDef.defaultValue,\n columnDef.type,\n )}`,\n );\n }\n\n sql += `${parts.join(' ')},\\n`;\n }\n\n return `${sql.slice(0, -2)}\\n);`;\n }\n\n /**\n * Format default value for SQL\n *\n * SQLite doesn't support CAST expressions in DEFAULT values (only literal values are allowed).\n * This method generates DEFAULT values that work with both SQLite and DuckDB.\n *\n * @param value - Default value\n * @param type - Column SQL type\n * @returns Formatted SQL default value expression\n */\n private formatDefaultValue(value: unknown, type: SQLDataType): string {\n // Delegate to the shared, injection-safe formatter so all DDL paths\n // converge on one set of rules (allowlisted keyword/function defaults\n // instead of \"contains `(`\", type-driven quoting, no string-\"null\" fold).\n // This path is engine-agnostic and never emits CAST expressions, so the\n // output stays valid for SQLite and DuckDB. Non-string TIMESTAMP defaults\n // fall back to lowercase `current_timestamp` to preserve prior output.\n return formatDefaultValueShared(value, type, {\n nonStringTimestampDefault: 'current_timestamp',\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -16,4 +16,124 @@ export declare function renderIndexTarget(index: Pick<IndexDefinition, 'columns'
|
|
|
16
16
|
* True if the index has a valid jsonPath target.
|
|
17
17
|
*/
|
|
18
18
|
export declare function isJsonPathIndex(index: Pick<IndexDefinition, 'jsonPath'>): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* True for a UNIQUE index whose predicate scopes it to one STI subtype
|
|
21
|
+
* (`WHERE _meta_type = '<qualified class>'`) — the shape `SchemaGenerator`
|
|
22
|
+
* emits for `@field({ unique: true })` declared only on an STI descendant
|
|
23
|
+
* (#2359).
|
|
24
|
+
*
|
|
25
|
+
* Engines without partial indexes (DuckDB, and the JSON adapter it backs)
|
|
26
|
+
* degrade an ordinary partial index to a full index, and a caller-declared
|
|
27
|
+
* partial UNIQUE (`WHERE active = TRUE`) to a full UNIQUE — a stricter but
|
|
28
|
+
* intended approximation. This shape is the exception: widening it would
|
|
29
|
+
* enforce one subtype's uniqueness across every sibling's rows in the shared
|
|
30
|
+
* table, so those engines skip it instead. Kept here so the DDL strategy and
|
|
31
|
+
* the migration differ apply the same test.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isStiSubtypeUniqueIndex(index: Pick<IndexDefinition, 'unique' | 'where'>): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The strictest identifier length any SMRT-supported engine imposes: 63 bytes.
|
|
36
|
+
*
|
|
37
|
+
* PostgreSQL compiles with `NAMEDATALEN = 64`, so every table, column, index,
|
|
38
|
+
* constraint and trigger name is silently truncated to 63 **bytes** (not
|
|
39
|
+
* characters — the limit is on the UTF-8 encoding). SQLite and DuckDB have no
|
|
40
|
+
* practical limit, which is exactly why the overflow went unnoticed: a name
|
|
41
|
+
* that works in every test truncates in production (#2374, finding C5).
|
|
42
|
+
*
|
|
43
|
+
* Silent truncation is not merely cosmetic. `CREATE INDEX IF NOT EXISTS` on a
|
|
44
|
+
* name whose first 63 bytes match an existing index is a no-op, so a second
|
|
45
|
+
* index that differs only past byte 63 is never created; the migration differ
|
|
46
|
+
* then finds it missing on every run and emits `add_index` forever. The shipped
|
|
47
|
+
* `content_contribution_revisions_contribution_id_revision_number_idx` (66
|
|
48
|
+
* bytes) is the concrete case — only the differ's signature-equivalence check
|
|
49
|
+
* kept it from looping.
|
|
50
|
+
*/
|
|
51
|
+
export declare const MAX_IDENTIFIER_BYTES = 63;
|
|
52
|
+
/**
|
|
53
|
+
* Byte length of `value` when encoded as UTF-8 — what PostgreSQL counts.
|
|
54
|
+
*
|
|
55
|
+
* Hand-rolled rather than `Buffer.byteLength` / `TextEncoder`: this module is
|
|
56
|
+
* re-exported from `schema/utils.ts`, which exists precisely to keep Node-only
|
|
57
|
+
* code out of browser builds, and allocating an encoder per identifier check
|
|
58
|
+
* would be wasteful besides.
|
|
59
|
+
*/
|
|
60
|
+
export declare function identifierByteLength(value: string): number;
|
|
61
|
+
/**
|
|
62
|
+
* Shorten a **generated** identifier to fit {@link MAX_IDENTIFIER_BYTES},
|
|
63
|
+
* deterministically and reversibly-by-inspection.
|
|
64
|
+
*
|
|
65
|
+
* A name that already fits is returned unchanged — the overwhelming majority,
|
|
66
|
+
* so existing databases see no churn. An over-long name becomes
|
|
67
|
+
* `<head>_<digest><suffix>`, where `suffix` is the recognisable trailing token
|
|
68
|
+
* (`_idx`, `_unique_idx`, …) when one is present and there is room for it, and
|
|
69
|
+
* `head` is the longest prefix of the remainder that leaves the digest room.
|
|
70
|
+
*
|
|
71
|
+
* The digest is taken over the **full original name**, so two names that share
|
|
72
|
+
* a 63-byte prefix — the case PostgreSQL would collapse into one index — get
|
|
73
|
+
* different digests and stay distinct.
|
|
74
|
+
*
|
|
75
|
+
* Only generator-owned names go through here. A developer's own name
|
|
76
|
+
* (`@smrt({ indexes: [{ name }] })`) is validated with
|
|
77
|
+
* {@link assertIdentifierFits} instead: silently renaming what someone wrote by
|
|
78
|
+
* hand would be worse than refusing it.
|
|
79
|
+
*
|
|
80
|
+
* @param name - The generated identifier
|
|
81
|
+
* @param maxBytes - Override the limit (tests; defaults to the PostgreSQL one)
|
|
82
|
+
* @returns `name` when it fits, otherwise its deterministic short form
|
|
83
|
+
* @throws When `maxBytes` is too small to hold even a digest — the one case
|
|
84
|
+
* where shortening is genuinely impossible
|
|
85
|
+
* @example
|
|
86
|
+
* // 66 bytes → 63
|
|
87
|
+
* shortenIdentifier('content_contribution_revisions_contribution_id_revision_number_idx')
|
|
88
|
+
*/
|
|
89
|
+
export declare function shortenIdentifier(name: string, maxBytes?: number): string;
|
|
90
|
+
/**
|
|
91
|
+
* Reject a **hand-declared index name** that cannot survive PostgreSQL.
|
|
92
|
+
*
|
|
93
|
+
* Scoped deliberately narrowly — to names SMRT neither owns nor ever compares
|
|
94
|
+
* loosely. A declared index name is both: the generator may not rewrite what a
|
|
95
|
+
* developer wrote, and `SchemaComparer` matches indexes **by name** first, so a
|
|
96
|
+
* 70-byte declaration can never match the 63-byte index PostgreSQL actually
|
|
97
|
+
* stored and `db:migrate` would emit `add_index` for it on every run, forever.
|
|
98
|
+
*
|
|
99
|
+
* This does NOT apply to table or column names, and deliberately so. PostgreSQL
|
|
100
|
+
* truncates identifiers *consistently on every reference* — `CREATE TABLE
|
|
101
|
+
* "<80 bytes>"` and a later `SELECT ... FROM "<the same 80 bytes>"` both resolve
|
|
102
|
+
* to the same stored 63-byte name — so a single long table name round-trips
|
|
103
|
+
* fine end to end. `smrt-users` relies on exactly that: it ships an intentional
|
|
104
|
+
* 80-byte `@smrt({ tableName })` and generates unique Postgres RLS policy names
|
|
105
|
+
* from it. Erroring there would break a supported, tested case for no safety
|
|
106
|
+
* gain; the collision hazard that remains is a name the developer chose, not
|
|
107
|
+
* one the generator manufactured.
|
|
108
|
+
*
|
|
109
|
+
* @param name - The identifier to check
|
|
110
|
+
* @param kind - What it is, for the message (`Declared index`, …)
|
|
111
|
+
* @param context - Where it came from (`table "x"`), for the message
|
|
112
|
+
* @param maxBytes - Override the limit (tests)
|
|
113
|
+
* @throws When `name` exceeds the limit
|
|
114
|
+
*/
|
|
115
|
+
export declare function assertIdentifierFits(name: string, kind: string, context: string, maxBytes?: number): void;
|
|
116
|
+
/**
|
|
117
|
+
* Apply the identifier guard to one generated schema, in place.
|
|
118
|
+
*
|
|
119
|
+
* Call this as the LAST step of every schema path, after every index pass has
|
|
120
|
+
* run, so it sees the final set. Shortening is safe to do here rather than at
|
|
121
|
+
* each `indexes.push()`: the digest is taken over the full original name, so
|
|
122
|
+
* two entries that were distinct before shortening stay distinct after, and the
|
|
123
|
+
* "does this name already exist" dedupe the passes perform upstream is
|
|
124
|
+
* unaffected.
|
|
125
|
+
*
|
|
126
|
+
* Only index names are touched. The table name is taken as given — see
|
|
127
|
+
* {@link assertIdentifierFits} on why a long table name is not an error — and
|
|
128
|
+
* is used solely to build the collision message. Note that an over-long table
|
|
129
|
+
* name still yields *in-limit, distinct* index names, because the shortening
|
|
130
|
+
* runs over the whole composed name.
|
|
131
|
+
*
|
|
132
|
+
* @param tableName - The table these indexes belong to, for the message
|
|
133
|
+
* @param indexes - The generated index list (names are shortened in place)
|
|
134
|
+
* @throws When shortening two index names produces a collision
|
|
135
|
+
*/
|
|
136
|
+
export declare function enforceIdentifierLimits(tableName: string, indexes: Array<{
|
|
137
|
+
name: string;
|
|
138
|
+
}>): void;
|
|
19
139
|
//# sourceMappingURL=index-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-utils.d.ts","sourceRoot":"","sources":["../../src/schema/index-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,UAAU,CAAC,EACpD,MAAM,EAAE,cAAc,GACrB,MAAM,CAiCR;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,GACvC,OAAO,CAET"}
|
|
1
|
+
{"version":3,"file":"index-utils.d.ts","sourceRoot":"","sources":["../../src/schema/index-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,UAAU,CAAC,EACpD,MAAM,EAAE,cAAc,GACrB,MAAM,CAiCR;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,GACvC,OAAO,CAET;AAcD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,OAAO,CAAC,GAC/C,OAAO,CAMT;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AA2BvC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO1D;AA+FD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAA6B,GACtC,MAAM,CA4BR;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAA6B,GACtC,IAAI,CAQN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC/B,IAAI,CAcN"}
|