@rebasepro/server-postgres 0.22.0 → 0.23.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/dist/{BranchService-CucnFcSE.js → BranchService-DGPL6G_C.js} +2 -2
- package/dist/{BranchService-CucnFcSE.js.map → BranchService-DGPL6G_C.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +41 -1
- package/dist/auth/services.d.ts +27 -8
- package/dist/backup/backup-cli.d.ts +22 -0
- package/dist/{backup-cli-DqBakiMO.js → backup-cli-24v4OlSp.js} +17 -14
- package/dist/backup-cli-24v4OlSp.js.map +1 -0
- package/dist/{backup-service-CaMOS76G.js → backup-service-HQ9GC4tN.js} +2 -2
- package/dist/{backup-service-CaMOS76G.js.map → backup-service-HQ9GC4tN.js.map} +1 -1
- package/dist/{cli-errors-Dka89exj.js → cli-errors-B8qHg02P.js} +6 -1
- package/dist/cli-errors-B8qHg02P.js.map +1 -0
- package/dist/cli-flags-BglvpjHv.js +138 -0
- package/dist/cli-flags-BglvpjHv.js.map +1 -0
- package/dist/cli-flags.d.ts +28 -0
- package/dist/cli-helpers.d.ts +6 -0
- package/dist/cli-scratch-database.d.ts +34 -0
- package/dist/cli.js +97 -261
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-DF-8dTVa.js} +16 -20
- package/dist/column-plan-helpers-DF-8dTVa.js.map +1 -0
- package/dist/{doctor-CU9IogdL.js → doctor-Cb2thZ8s.js} +8 -7
- package/dist/doctor-Cb2thZ8s.js.map +1 -0
- package/dist/{ensure-collection-policies-Dzd-2S81.js → ensure-collection-policies-RHUcEp4v.js} +5 -5
- package/dist/{ensure-collection-policies-Dzd-2S81.js.map → ensure-collection-policies-RHUcEp4v.js.map} +1 -1
- package/dist/{ensure-collection-tables-CpAgy51F.js → ensure-collection-tables-BEEjn5cn.js} +53 -22
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +1 -0
- package/dist/{ensure-tables-Cr5B4UmH.js → ensure-tables-Dhn9KM3B.js} +2 -2
- package/dist/{ensure-tables-Cr5B4UmH.js.map → ensure-tables-Dhn9KM3B.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js → generate-drizzle-schema-logic-BfzK7UQd.js} +2 -2
- package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js.map → generate-drizzle-schema-logic-BfzK7UQd.js.map} +1 -1
- package/dist/{generate-drizzle-schema-B537GIvz.js → generate-drizzle-schema-yzY_BLhr.js} +2 -2
- package/dist/{generate-drizzle-schema-B537GIvz.js.map → generate-drizzle-schema-yzY_BLhr.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-D7imhYV8.js → generate-postgres-ddl-logic-Bt2d2mRH.js} +2 -2
- package/dist/{generate-postgres-ddl-logic-D7imhYV8.js.map → generate-postgres-ddl-logic-Bt2d2mRH.js.map} +1 -1
- package/dist/index.es.js +1508 -355
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-WMuAfxvw.js} +323 -4
- package/dist/introspect-db-logic-WMuAfxvw.js.map +1 -0
- package/dist/{plan-schema-CboAIwLN.js → plan-schema-C0fxM8dY.js} +158 -31
- package/dist/plan-schema-C0fxM8dY.js.map +1 -0
- package/dist/{policy-drift-xJfy9xG7.js → policy-drift-DljYdrpW.js} +2 -2
- package/dist/{policy-drift-xJfy9xG7.js.map → policy-drift-DljYdrpW.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-H3rCFi3F.js → rls-bootstrap-sql-B8EclDyM.js} +320 -17
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +1 -0
- package/dist/schema/atlas-argv.d.ts +15 -0
- package/dist/schema/column-plan-helpers.d.ts +23 -4
- package/dist/schema/destructive-sql.d.ts +52 -1
- package/dist/schema/doctor-cli.js +3 -3
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/introspect-db.js +1 -312
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +3 -2
- package/dist/schema/plan/plan-schema.d.ts +10 -3
- package/dist/schema/plan/types.d.ts +6 -1
- package/dist/services/FetchService.d.ts +97 -28
- package/dist/services/PersistService.d.ts +41 -5
- package/dist/services/RelationService.d.ts +11 -0
- package/dist/services/RelationWriteService.d.ts +6 -0
- package/dist/services/cdc/CdcListener.d.ts +6 -1
- package/dist/services/collection-helpers.d.ts +11 -0
- package/dist/services/dataService.d.ts +3 -20
- package/dist/services/field-op-sql.d.ts +71 -0
- package/dist/services/junction-writes.d.ts +19 -3
- package/dist/services/pg-notify-listener.d.ts +7 -0
- package/dist/services/read-field-access.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +77 -3
- package/dist/services/row-pipeline.d.ts +5 -0
- package/dist/services/soft-delete.d.ts +0 -2
- package/dist/services/write-transaction-scope.d.ts +39 -0
- package/dist/utils/drizzle-conditions.d.ts +7 -2
- package/dist/websocket.d.ts +10 -3
- package/package.json +9 -9
- package/dist/backup-cli-DqBakiMO.js.map +0 -1
- package/dist/cli-errors-Dka89exj.js.map +0 -1
- package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
- package/dist/doctor-CU9IogdL.js.map +0 -1
- package/dist/ensure-collection-tables-CpAgy51F.js.map +0 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
- package/dist/plan-schema-CboAIwLN.js.map +0 -1
- package/dist/rls-bootstrap-sql-H3rCFi3F.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-collection-tables-CpAgy51F.js","names":[],"sources":["../src/schema/column-type-drift.ts","../src/schema/plan/diff-plan.ts","../src/schema/ensure-collection-tables.ts"],"sourcesContent":["/**\n * A column whose type in the database is not the type the collection declares.\n *\n * The boot ensure is additive: it creates tables, columns, enum values and\n * indexes, and it never changes an existing column's type. That is the right\n * default — an automatic `ALTER COLUMN … TYPE` against customer data, decided\n * by a process nobody is watching, is not a thing to do quietly.\n *\n * What was wrong is that it happened *silently*. A property's `columnType`\n * changed after the first deploy, the deploy reported success, the column stayed\n * as it was, and the divergence surfaced later as writes failing:\n *\n * Invalid data format in \"listing_observations\":\n * malformed array literal: \"[0,0,0.0755,…]\"\n *\n * ...after a 10,690-row import, from a deployment that had said \"success\". The\n * two ends disagreed about the schema, so no value satisfied both: the declared\n * shape was rejected by Postgres and the Postgres shape was rejected by the\n * API's own validator, first. There is no encoding out of that, only a report.\n *\n * ## Why the comparison is deliberately blunt\n *\n * A drift report that cries wolf is worse than none, because the boot that says\n * it every time is the boot nobody reads. So types are compared by *family*, and\n * a family is only split from another when the difference changes what the\n * column can hold:\n *\n * - every numeric type is one family. `int4`, `numeric` and `float8` all arrive\n * as JavaScript numbers and accept each other's values; a database adopted\n * from an existing schema legitimately has `int4` where `type: \"number\"`\n * generates `NUMERIC`, and that is not news.\n * - every character type is one family, for the same reason: `text`,\n * `varchar(255)` and `char(3)` differ in what they *reject*, not in shape.\n * - `jsonb` and an array are different families, and that is the case this\n * exists for.\n *\n * Anything this module does not recognise — an enum type, a domain, a PostGIS\n * geometry, an extension type it has never met — returns `null` and is compared\n * with nothing. Silence on an unknown type is the deliberate choice: the cost of\n * a missed report is a question somebody asks once, and the cost of a false one\n * is a warning everybody learns to skip.\n */\n\n/** A column whose live type and declared type are in different families. */\nexport interface ColumnTypeDrift {\n /** `schema.table`. */\n table: string;\n /** The column name as it exists in the database. */\n column: string;\n /** What the collection says the column should be, e.g. `JSONB`. */\n declared: string;\n /** What the database actually has, as `udt_name` reports it, e.g. `_numeric`. */\n actual: string;\n}\n\n/**\n * Types that mean the same thing to everything above the driver.\n *\n * Keyed by every spelling either side can produce: `information_schema` reports\n * `udt_name` (`int4`, `_numeric`, `bpchar`), while the generators emit SQL\n * (`INTEGER`, `NUMERIC[]`, `CHAR(3)`). Both are normalised through here.\n */\nconst FAMILIES: Record<string, string> = {\n // Numbers. One family on purpose — see the note above.\n int2: \"number\", smallint: \"number\",\n int4: \"number\", int: \"number\", integer: \"number\",\n int8: \"number\", bigint: \"number\",\n numeric: \"number\", decimal: \"number\",\n float4: \"number\", real: \"number\",\n float8: \"number\", \"double precision\": \"number\",\n money: \"number\",\n\n // Characters. Also one family: the differences are limits, not shapes.\n text: \"text\", varchar: \"text\", \"character varying\": \"text\",\n bpchar: \"text\", char: \"text\", character: \"text\", citext: \"text\",\n\n bool: \"boolean\", boolean: \"boolean\",\n\n json: \"json\",\n jsonb: \"jsonb\",\n\n uuid: \"uuid\",\n bytea: \"bytea\",\n\n date: \"date\",\n time: \"time\", \"time without time zone\": \"time\",\n timetz: \"time\", \"time with time zone\": \"time\",\n timestamp: \"timestamp\", \"timestamp without time zone\": \"timestamp\",\n timestamptz: \"timestamptz\", \"timestamp with time zone\": \"timestamptz\",\n interval: \"interval\",\n\n tsvector: \"tsvector\",\n vector: \"vector\",\n halfvec: \"halfvec\",\n sparsevec: \"sparsevec\"\n};\n\n/**\n * One SQL or `udt_name` spelling reduced to its family, or `null` when this\n * module has no opinion about it.\n *\n * `null` is not a failure. It is the answer for an enum type, a domain, a\n * PostGIS column, a quoted qualified name — everything whose identity is a\n * *name* rather than a shape, where two spellings being different says nothing\n * about whether the values are compatible.\n */\nexport function typeFamily(raw: string | undefined | null): string | null {\n if (!raw) return null;\n let type = raw.trim().toLowerCase();\n if (!type) return null;\n\n // `\"public\".\"posts_status\"` — an enum, identified by name. Not comparable.\n if (type.startsWith(\"\\\"\")) return null;\n\n // `INTEGER GENERATED BY DEFAULT AS IDENTITY` is an `int4` with a default\n // attached; the identity clause is not part of the type.\n const generated = type.indexOf(\" generated \");\n if (generated > 0) type = type.slice(0, generated).trim();\n\n // Arrays, in both spellings: `NUMERIC[]` from the generators, `_numeric`\n // from `udt_name`. Compared element-wise, so `text[]` and `numeric[]` are\n // different and `int4[]` and `numeric[]` are not.\n if (type.endsWith(\"[]\")) {\n const element = typeFamily(type.slice(0, -2));\n return element && `array:${element}`;\n }\n if (type.startsWith(\"_\")) {\n const element = typeFamily(type.slice(1));\n return element && `array:${element}`;\n }\n\n // `VARCHAR(255)`, `VECTOR(384)`, `NUMERIC(10, 2)` — the width is a limit on\n // a family, not a family. A vector whose *dimensions* changed is a separate\n // concern with its own handling in the ensure.\n const paren = type.indexOf(\"(\");\n if (paren > 0) type = type.slice(0, paren).trim();\n\n return FAMILIES[type] ?? null;\n}\n\n/**\n * Do these two spellings describe columns that hold the same kind of value?\n *\n * `true` whenever the answer is not confidently \"no\" — an unrecognised type on\n * either side agrees with everything, because a report needs evidence and an\n * unknown type is the absence of it.\n */\nexport function typesAgree(declared: string, actual: string): boolean {\n const a = typeFamily(declared);\n const b = typeFamily(actual);\n if (a === null || b === null) return true;\n if (a !== b) return false;\n // Same family, and for `numeric` that is not the end of it: the modifier is\n // what makes `NUMERIC(10, 2)` a price and a bare `NUMERIC` a number that\n // keeps whatever the caller sent. A property that declares\n // `precision`/`scale` is asking the database to round, and a column without\n // them silently does not — so the two disagree, and the drift report says\n // so. A declaration with no modifier accepts whatever is there, which is\n // what it always did.\n if (a !== \"numeric\") return true;\n const declaredModifier = numericModifier(declared);\n if (declaredModifier === null) return true;\n return declaredModifier === numericModifier(actual);\n}\n\n/** `numeric(10,2)` / `NUMERIC(10, 2)` → `10,2`; a bare `numeric` → `null`. */\nfunction numericModifier(raw: string): string | null {\n const match = raw.match(/\\(([^)]*)\\)/);\n return match ? match[1].replace(/\\s+/g, \"\") : null;\n}\n\n/**\n * The sentence a boot log says about one drifted column.\n *\n * Written to be actionable in the place it is read, which is a runtime log with\n * no way to run a migration from it: it names both types, says which surface\n * will break first, and gives the statement that resolves it.\n */\nexport function columnTypeDriftMessage(drift: ColumnTypeDrift): string {\n const [schema, table] = drift.table.split(\".\");\n return (\n `Column \"${drift.table}\".\"${drift.column}\" is ${drift.actual} in the database, but this ` +\n `collection declares ${drift.declared}. Boot does not change an existing column's type, so ` +\n \"the database keeps what it has and writes that match the declaration will be rejected by \" +\n \"Postgres. Change it deliberately, when you can watch it:\\n\" +\n ` ALTER TABLE \"${schema}\".\"${table}\" ALTER COLUMN \"${drift.column}\" ` +\n `TYPE ${drift.declared} USING \"${drift.column}\"::${drift.declared};`\n );\n}\n","/**\n * The additive statements that bring a database up to a {@link SchemaPlan}.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, at boot, against a database with customers' data in it,\n * with no human reading a diff. So it may only ever do things that cannot lose\n * data: create a missing table, add a missing column, create a missing enum\n * type, add an enum value, create an index.\n *\n * It will **never** drop a table or a column, narrow a type, or alter a\n * constraint at boot. A removed field leaves its column behind; a renamed field\n * looks like an addition and the old column stays. That is the correct trade\n * for an automated path — the alternative is an unattended process that can\n * silently destroy a column. Destructive changes stay a deliberate,\n * human-reviewed migration. Because of that, this is safe to run on every boot,\n * and re-running it is a no-op.\n *\n * ## What changed when the planner arrived\n *\n * This used to read `Property` itself — its own reading of the type, of\n * `validation`, of relations, of enums — and disagreed with `db push` about all\n * of them. The audit found a required `author_id` that was NOT NULL after a\n * push and nullable after a boot, on the one path with no developer in the\n * loop. Now it reads the same {@link ColumnPlan} the DDL renderer does, so a\n * column's type, default, uniqueness and foreign key are decided once. What is\n * left here is the half that is genuinely this path's own: *what the database\n * already has*, and which of the planned constraints are safe to apply to it.\n */\nimport type { ColumnPlan, SchemaPlan, TablePlan } from \"./types\";\nimport { renderColumnDefinition, renderPgType } from \"./render-ddl\";\nimport { quoteSqlLiteral } from \"./plan-schema\";\nimport { SET_UPDATED_AT_FN, triggerStatements } from \"./updated-at-trigger\";\nimport {\n SEARCH_STAMP_PREFIX,\n SEARCH_TEXT_FN,\n SEARCH_UNACCENT_FN,\n searchColumnStamps,\n searchIndexStatements\n} from \"../search-column\";\nimport { collectionIndexStatement, sortIndexSpecs } from \"../collection-index\";\nimport { vectorIndexStatement, type SkippedVectorIndex } from \"../vector-index\";\nimport { typesAgree, type ColumnTypeDrift } from \"../column-type-drift\";\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/**\n * Validation used to run only on the names read back OUT of the catalogue,\n * which is the direction that cannot hurt anyone: those came from Postgres. The\n * names going IN — the schema and table a collection declares — were quoted and\n * concatenated on trust, and quoting is not escaping. A table named\n * `x\" (id int); DROP TABLE users; --` closes the quote and the statement, and\n * the whole thing runs on the owner connection, which is the one connection in\n * the system that is exempt from every RLS policy.\n *\n * That is only a config file on a self-hosted project, where the person writing\n * it could run the SQL directly anyway. It is not only a config file where a\n * collection can be defined over the wire — the live-schema and source editors\n * both do that — and there the caller is an admin on the app, not an operator\n * of the database.\n */\nexport function assertSafeIdentifier(value: string, what: string): string {\n if (!SAFE_IDENTIFIER.test(value)) {\n throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);\n }\n return value;\n}\n\n/** What the database currently has, as the planner needs it. */\nexport interface ExistingSchema {\n /** `schema.table` → set of column names. */\n tables: Map<string, Set<string>>;\n /** `schema.typename` of every enum type that already exists. */\n enums: Set<string>;\n /**\n * `schema.table.constraint` of every constraint that already exists.\n *\n * Optional so a caller that only cares about tables can still build one by\n * hand; absent is read as \"none known\", which at worst re-attempts a\n * constraint that then fails harmlessly as a duplicate.\n */\n constraints?: Set<string>;\n /**\n * `schema.table.column` → that column's comment, for the columns that have\n * one. This is where a generated search column's fingerprint lives.\n */\n columnComments?: Map<string, string>;\n /** `schema.typename` → its labels, in `enumsortorder`. */\n enumValues?: Map<string, string[]>;\n /** `schema.table.column` of every NOT NULL column. */\n notNullColumns?: Set<string>;\n /** `schema.table.column` of every column that has a DEFAULT. */\n columnDefaults?: Set<string>;\n /** `schema.table` of every table that holds at least one row. */\n populatedTables?: Set<string>;\n /**\n * `schema.table.column` → the type Postgres reports, as `udt_name` (plus a\n * modifier where one distinguishes two columns of the same family, e.g.\n * `numeric(10,2)`).\n */\n columnTypes?: Map<string, string>;\n /** `schema.table.trigger` of every trigger that already exists. */\n triggers?: Set<string>;\n}\n\n/**\n * How far the planner may go in making the database's constraints match the\n * configuration.\n *\n * - `additive` — the boot default. Columns, tables, indexes and enum values are\n * created; no existing column's constraints are touched. Unattended boots run\n * against customer data with nobody reading a diff, and a database adopted by\n * introspection legitimately carries NOT NULL on columns the generated\n * collection leaves optional (`introspect-db-logic` withholds `required` from\n * a column with a default or a trigger behind it). Converging there would\n * strip real constraints on first boot.\n * - `converge` — the live schema editor. Every statement is planned, shown to\n * the person making the change, and applied only once they confirm it.\n */\nexport type ConstraintPolicy = \"additive\" | \"converge\";\n\nexport interface DiffOptions {\n /** Defaults to `additive`. See {@link ConstraintPolicy}. */\n constraints?: ConstraintPolicy;\n}\n\nexport interface EnsureAction {\n kind: \"create-enum\" | \"create-table\" | \"add-column\" | \"add-constraint\" | \"rename-column\"\n | \"create-extension\" | \"create-function\" | \"create-index\" | \"comment-column\"\n | \"add-enum-value\" | \"set-not-null\" | \"drop-not-null\" | \"set-default\" | \"create-trigger\";\n /** Qualified target, for logging: `public.posts` or `public.posts.title`. */\n target: string;\n sql: string;\n}\n\n/** A NOT NULL column with no default that no property declares. */\nexport interface OrphanedRequiredColumn {\n /** `schema.table`. */\n table: string;\n /** The column left behind. */\n column: string;\n /** The collection that no longer declares it. */\n slug: string;\n}\n\n/** A constraint the configuration asks for that the planner is not applying. */\nexport interface WithheldConstraint {\n /** `schema.table.column`. */\n target: string;\n kind: \"not-null\";\n /**\n * Why, in a sentence that names the obstacle rather than the rule. The\n * reader is looking at a column that is nullable when they asked for\n * required, and needs to know what to do about it.\n */\n reason: string;\n /** What would make it applicable. */\n remedy: string;\n}\n\n/**\n * A generated search column built from a `search` block that has since changed.\n *\n * Reported instead of applied because the two ways to apply it are both worse\n * than stopping. `ALTER COLUMN … SET EXPRESSION` exists only on PG17+ and\n * rewrites the table either way; `DROP COLUMN` + `ADD COLUMN` rewrites it under\n * an ACCESS EXCLUSIVE lock and rebuilds the GIN index. This module runs\n * unattended against live customer data with nobody reading a diff, so a\n * multi-minute outage is not a decision it may take on its own.\n *\n * Not applying it silently is not an option either: a collection that added a\n * field, flipped `unaccent` or raised a weight kept indexing the *old* set\n * forever, and the only symptom was searches returning nothing for content\n * plainly in the row.\n */\nexport interface SearchColumnDrift {\n /** `schema.table`. */\n table: string;\n column: string;\n /** The fingerprint recorded on the column. */\n found: string;\n /** The fingerprint the current `search` block computes. */\n expected: string;\n /** The statements that would rebuild the column, for the operator to run. */\n rebuild: string[];\n}\n\n/** A relation column whose old and new spellings both plausibly apply. */\nexport interface LegacyForeignKey {\n /** `schema.table`. */\n table: string;\n /** The name the current rule derives, and what this plan would create. */\n expected: string;\n /** The name the old rule derived, which the table already has. */\n legacy: string;\n}\n\nexport interface EnsurePlan {\n actions: EnsureAction[];\n /** Every statement, in dependency order. Empty when the schema is current. */\n statements: string[];\n /**\n * Relation columns this plan is about to create where the table already\n * carries the same column under its pre-singularization name.\n *\n * Reported rather than silently handled: the ensure is additive, so it\n * would add `category_id` beside a populated `categorie_id` and the\n * relation would then read the new, empty one. No statement fails, no table\n * is missing, and the only symptom is relations resolving to nothing.\n */\n legacyForeignKeys: LegacyForeignKey[];\n /** @see SearchColumnDrift */\n searchDrift: SearchColumnDrift[];\n /**\n * Generated search columns that exist but carry no fingerprint — created\n * before this check existed, or by `search.sql` on an older CLI. The plan\n * stamps them so the *next* change is detectable.\n */\n searchAdopted: { table: string; column: string }[];\n /**\n * Vector columns this plan is deliberately leaving unindexed, because\n * pgvector cannot build an ANN index that wide. Reported rather than\n * thrown: the column is valid, storable and searchable.\n */\n vectorIndexSkipped: SkippedVectorIndex[];\n /**\n * Constraints the configuration asks for that this plan is not applying,\n * and why. Every one of these was previously withheld in silence.\n */\n withheldConstraints: WithheldConstraint[];\n /**\n * Columns whose type in the database is in a different family from the one\n * the collection declares. Reported and never acted on — this is the one\n * divergence that presents as a *working* deploy.\n */\n columnTypeDrift: ColumnTypeDrift[];\n /**\n * Columns that are NOT NULL, have no default, and that no property declares\n * any more — so every insert through the API is rejected for a field the\n * author cannot name, because they already deleted it. This is what a\n * **rename** looks like to an additive provisioner. Reported, never acted\n * on: dropping the column is the right fix roughly always and destructive\n * exactly once, which is not a decision to take unattended.\n */\n orphanedRequiredColumns: OrphanedRequiredColumn[];\n}\n\n/** Which pass a column belongs to. The order below is the order they run in. */\nconst isScalarProperty = (column: ColumnPlan): boolean =>\n (column.source.kind === \"property\" || column.source.kind === \"implicit-id\") && !column.primaryKey;\n\nexport function diffPlanAgainstCatalogue(\n plan: SchemaPlan,\n existing: ExistingSchema,\n options: DiffOptions = {}\n): EnsurePlan {\n const constraintPolicy: ConstraintPolicy = options.constraints ?? \"additive\";\n const actions: EnsureAction[] = [];\n const withheldConstraints: WithheldConstraint[] = [];\n const columnTypeDrift: ColumnTypeDrift[] = [];\n const legacyForeignKeys: LegacyForeignKey[] = [];\n const searchDrift: SearchColumnDrift[] = [];\n const searchAdopted: { table: string; column: string }[] = [];\n\n const collectionTables = plan.tables.filter(t => t.kind === \"collection\");\n const junctionTables = plan.tables.filter(t => t.kind === \"junction\");\n\n // 1. Enum types. `CREATE TYPE` has no IF NOT EXISTS, so an existing type is\n // skipped by name rather than guarded in SQL.\n for (const enumPlan of plan.enums) {\n assertSafeIdentifier(enumPlan.schema, \"schema name\");\n if (existing.enums.has(enumPlan.qualified)) {\n // The type is there, but that says nothing about its *values*.\n // Skipping the whole type by name is what made an added enum value\n // vanish: nothing was planned, the boot reported success, and the\n // first row using the value was rejected by a type that had never\n // heard of it. `ADD VALUE` is the one alteration Postgres offers\n // here, it is purely additive, and it is idempotent.\n //\n // `enumValues` absent means the caller built the schema by hand and\n // does not know the values; skip by name as before.\n const current = existing.enumValues?.get(enumPlan.qualified);\n if (!current) continue;\n for (const value of enumPlan.labels) {\n if (current.includes(value)) continue;\n actions.push({\n kind: \"add-enum-value\",\n target: `${enumPlan.qualified}.${value}`,\n // Not inside a transaction with any use of the value:\n // Postgres refuses to read a value added by the transaction\n // still adding it. The applier runs these one statement at a\n // time, which is what makes it legal.\n sql: `ALTER TYPE \"${enumPlan.schema}\".\"${enumPlan.name}\" ADD VALUE IF NOT EXISTS ${quoteSqlLiteral(value)};`\n });\n }\n continue;\n }\n actions.push({\n kind: \"create-enum\",\n target: enumPlan.qualified,\n sql: `CREATE TYPE \"${enumPlan.schema}\".\"${enumPlan.name}\" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(\", \")});`\n });\n }\n\n // 1b. Extensions and helper functions, before the tables and columns that\n // reference them: a generated column's expression is resolved when the\n // column is created, so a table whose search column calls\n // `rebase_search_text` cannot be added before that function exists.\n // Both forms are idempotent, so a boot against a database that already\n // has them plans nothing.\n for (const statement of plan.extensions) {\n actions.push({\n kind: \"create-extension\",\n target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, \"\"),\n sql: statement\n });\n }\n for (const statement of plan.functions) {\n actions.push({ kind: \"create-function\", target: functionTarget(statement), sql: statement });\n }\n\n // 2. Missing tables. Only the identity column is created here; every other\n // column is added by step 3, so a new table and an existing table that\n // gained a field travel the exact same code path. One way to build a\n // column means one way for it to be wrong.\n const created = new Set<string>();\n for (const table of collectionTables) {\n if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;\n created.add(table.qualified);\n const schema = assertSafeIdentifier(table.schema, \"schema name\");\n const name = assertSafeIdentifier(table.table, \"table name\");\n const id = table.columns.find(c => c.primaryKey);\n const definition = id\n ? `\"${assertSafeIdentifier(id.column, \"column name\")}\" ${renderColumnDefinition(id)}`\n : '\"id\" TEXT PRIMARY KEY';\n actions.push({\n kind: \"create-table\",\n target: table.qualified,\n sql: `CREATE TABLE IF NOT EXISTS \"${schema}\".\"${name}\" (${definition});`\n });\n }\n\n // 2b. Junction tables behind many-to-many relations. No collection declares\n // them, so until they existed an m2m write had nowhere to land and the\n // junction's derived RLS had nothing to attach to. Both columns are\n // created here — unlike a collection table, whose CREATE declares only\n // the identity column — because there is nothing else to them.\n for (const table of junctionTables) {\n if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;\n created.add(table.qualified);\n const columns = table.columns\n .map(c => `\"${c.column}\" ${renderColumnDefinition(c)}`)\n .join(\", \");\n actions.push({\n kind: \"create-table\",\n target: table.qualified,\n sql: `CREATE TABLE IF NOT EXISTS \"${table.schema}\".\"${table.table}\" (${columns}` +\n `, PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")}));`\n });\n }\n\n const addColumn = (table: TablePlan, column: string, definition: string): void => {\n if (existing.tables.get(table.qualified)?.has(column)) return;\n // Same reason as `assertSafeIdentifier` above: a column name reaching\n // here came from a property declaration, which on the editor paths came\n // over the wire. `definition` is built by the renderer from a closed set\n // of type mappings and is not caller text.\n assertSafeIdentifier(column, \"column name\");\n actions.push({\n kind: \"add-column\",\n target: `${table.qualified}.${column}`,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ADD COLUMN IF NOT EXISTS \"${column}\" ${definition};`\n });\n };\n\n /**\n * Move a column that is only missing because it was renamed.\n *\n * Returns true when it handled the column, so the caller skips the ordinary\n * ADD. Adding would be the wrong move and a quiet one: the data is in the\n * old column, `ADD COLUMN` creates the new one empty beside it, every\n * statement succeeds, and the relation reads the empty one. A rename is\n * metadata-only in Postgres, keeps the values, and carries the column's\n * indexes and constraints with it.\n */\n const renameLegacyColumn = (table: TablePlan, column: ColumnPlan): boolean => {\n const present = existing.tables.get(table.qualified);\n if (!column.legacyColumn || !present) return false;\n if (present.has(column.column) || !present.has(column.legacyColumn)) return false;\n legacyForeignKeys.push({ table: table.qualified, expected: column.column, legacy: column.legacyColumn });\n actions.push({\n kind: \"rename-column\",\n target: `${table.qualified}.${column.column}`,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" RENAME COLUMN \"${column.legacyColumn}\" TO \"${column.column}\";`\n });\n return true;\n };\n\n /**\n * A `NOT NULL` is safe exactly when it cannot fail against rows that are\n * already there, and there are three ways to know that:\n *\n * - the table is being created by this plan (no rows yet);\n * - the table exists and is empty;\n * - the column arrives with a DEFAULT, which Postgres backfills into every\n * existing row as part of ADD COLUMN.\n *\n * `populatedTables` absent means the caller does not know, and not knowing\n * has to read as \"assume rows\" — the other direction emits a NOT NULL that\n * is checked against live data and aborts the boot. Written as an explicit\n * `!== undefined` because the optional-chain form quietly says *empty* when\n * the fact is missing, which is the wrong way to be wrong.\n */\n const tableIsEmpty = (table: TablePlan): boolean =>\n existing.populatedTables !== undefined\n && existing.tables.has(table.qualified)\n && !existing.populatedTables.has(table.qualified);\n\n /**\n * Plan one column, with the constraints this database can safely take.\n *\n * The definition comes from the same renderer `schema.sql` uses, minus the\n * constraints that are checked against live rows. That subtraction is the\n * whole of what this path is allowed to decide for itself.\n */\n const planColumn = (table: TablePlan, column: ColumnPlan): void => {\n const key = `${table.qualified}.${column.column}`;\n const columnExists = existing.tables.get(table.qualified)?.has(column.column) === true;\n const fresh = created.has(table.qualified);\n const empty = tableIsEmpty(table);\n const required = !column.nullable;\n const hasDefault = column.default !== undefined;\n\n // The column is there and holds a different kind of value than the\n // collection says it does. Reported, never altered: an unattended\n // `ALTER COLUMN … TYPE` over customer data is not a thing to do\n // quietly, and the *quiet* is what this fixes. A deploy that changed a\n // `columnType` used to report success while the column stayed as it\n // was, and the divergence surfaced later as every write failing.\n // A column whose definition is owned elsewhere (auth, or a generated\n // search column) is not compared: the declaration there is a rendered\n // string rather than a type, and auth's own reconcile owns it.\n const declaredType = renderPgType(column.type);\n const actualType = existing.columnTypes?.get(key);\n if (columnExists && !column.sqlDefinition && actualType && !typesAgree(declaredType, actualType)) {\n columnTypeDrift.push({ table: table.qualified, column: column.column, declared: declaredType, actual: actualType });\n }\n\n // A table this run is creating has no rows yet, so the constraints\n // `db push` writes are free to apply. On a table that already exists\n // they are not: `SET NOT NULL` is checked against live rows and a UNIQUE\n // would fail on existing duplicates. So the constraints are emitted for\n // the fresh case — which is the whole managed-runtime path, and the one\n // that diverged from `db push` — and withheld for the adopted one.\n const notNullIsSafe = fresh || empty || hasDefault;\n const applicable: ColumnPlan = {\n ...column,\n unique: column.unique && fresh,\n nullable: column.nullable || !(notNullIsSafe || columnExists)\n };\n if (required && !columnExists && !notNullIsSafe) {\n withheldConstraints.push({\n target: key,\n kind: \"not-null\",\n reason: column.source.kind === \"relation\" || column.source.kind === \"reference\"\n ? `\"${column.column}\" is a required link, but \"${table.qualified}\" already holds rows and ` +\n \"the column has no default to backfill them with, so NOT NULL would be checked \" +\n \"against data that does not have a value yet.\"\n : `\"${column.column}\" is required, but \"${table.qualified}\" already holds rows and the column ` +\n \"has no default to backfill them with, so NOT NULL would be checked \" +\n \"against data that does not have a value yet.\",\n remedy: column.source.kind === \"relation\" || column.source.kind === \"reference\"\n ? \"Backfill the column, then add the constraint — or make the relation optional.\"\n : \"Backfill the column, then add the constraint — or give the property a \" +\n \"default so every existing row gets one.\"\n });\n }\n addColumn(table, column.column, renderColumnDefinition(applicable));\n\n if (!columnExists) return;\n\n // ── The column is already there and only a modifier differs ──────────\n // A DEFAULT binds future writes only, so `SET DEFAULT` cannot fail and\n // cannot touch a row that already exists. That makes it the one\n // convergence this path may do unattended — and it has to, because\n // `ADD COLUMN IF NOT EXISTS` is a no-op against an existing column, so\n // a `defaultValue` added to a live collection would otherwise never\n // reach the database at all.\n if (column.default && column.default.kind !== \"identity\" && !column.generated) {\n const expression = column.default.kind === \"sql\" ? column.default.expression : column.default.sql;\n if (!existing.columnDefaults?.has(key)) {\n actions.push({\n kind: \"set-default\",\n target: key,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ALTER COLUMN \"${column.column}\" SET DEFAULT ${expression};`\n });\n }\n }\n\n // Two directions on NOT NULL, and they are not equally safe — see\n // `ConstraintPolicy` for why neither runs at an unattended boot.\n if (constraintPolicy !== \"converge\") return;\n const isNotNull = existing.notNullColumns?.has(key) === true;\n if (required && !isNotNull) {\n if (empty) {\n actions.push({\n kind: \"set-not-null\",\n target: key,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ALTER COLUMN \"${column.column}\" SET NOT NULL;`\n });\n } else {\n withheldConstraints.push({\n target: key,\n kind: \"not-null\",\n reason:\n `\"${column.column}\" became required, but \"${table.qualified}\" holds rows and any of them ` +\n \"with no value would make SET NOT NULL fail.\",\n remedy:\n \"Backfill the column first — `UPDATE … SET \\\"\" + column.column +\n \"\\\" = … WHERE \\\"\" + column.column + \"\\\" IS NULL` — then apply this again.\"\n });\n }\n }\n if (!required && isNotNull) {\n // Loosening never fails and never loses data. It is here rather\n // than at boot because a database adopted by introspection carries\n // NOT NULL on columns the generated collection deliberately leaves\n // optional, and converging those unasked would drop constraints\n // nobody edited.\n actions.push({\n kind: \"drop-not-null\",\n target: key,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ALTER COLUMN \"${column.column}\" DROP NOT NULL;`\n });\n }\n };\n\n // 3. Scalar columns, then the auth columns the collection never mentions.\n // The scaffold's users collection describes 12 of the 14 columns auth\n // reads and writes, so planning only from properties left `is_anonymous`\n // and `tokens_valid_after` to `ensureAuthTablesExist` — which does create\n // them, but only because that function happens to run later in the same\n // boot. Planning them here makes this path self-contained.\n for (const table of collectionTables) {\n assertSafeIdentifier(table.schema, \"schema name\");\n assertSafeIdentifier(table.table, \"table name\");\n for (const column of table.columns) {\n if (!isScalarProperty(column)) continue;\n planColumn(table, column);\n }\n for (const column of table.columns) {\n if (column.source.kind !== \"auth\") continue;\n addColumn(table, column.column, renderColumnDefinition(column));\n }\n }\n\n // 3a. The generated search columns.\n //\n // Adding a STORED generated column rewrites the table, which on a large\n // one is not free — but it is the same additive shape as every other\n // column here, and the alternative (leaving it out until someone runs a\n // migration) is a declared `search` block that silently does nothing.\n //\n // Changing one is not additive, and `ADD COLUMN IF NOT EXISTS` is a\n // no-op against a column that is already there — which is why a `search`\n // block that gained a field, flipped `unaccent` or moved a weight used\n // to be inert forever, on every path, with nothing logged. Each column\n // carries a fingerprint of the expression it was built from, and a\n // mismatch is reported rather than applied.\n for (const table of collectionTables) {\n if (!table.search) continue;\n const definitions = new Map(\n table.columns\n .filter(c => c.source.kind === \"search\")\n .map(c => [c.column, renderColumnDefinition(c)] as const)\n );\n for (const stamp of searchColumnStamps(table.search)) {\n const definition = definitions.get(stamp.column)!;\n const exists = existing.tables.get(table.qualified)?.has(stamp.column) === true;\n const recorded = existing.columnComments?.get(`${table.qualified}.${stamp.column}`);\n\n if (exists && recorded?.startsWith(SEARCH_STAMP_PREFIX) && recorded !== stamp.fingerprint) {\n searchDrift.push({\n table: table.qualified,\n column: stamp.column,\n found: recorded,\n expected: stamp.fingerprint,\n rebuild: [\n `ALTER TABLE \"${table.schema}\".\"${table.table}\" DROP COLUMN \"${stamp.column}\";`,\n `ALTER TABLE \"${table.schema}\".\"${table.table}\" ADD COLUMN \"${stamp.column}\" ${definition};`,\n stamp.sql\n ]\n });\n // The old stamp is the only evidence of what the column holds;\n // overwriting it here would erase the drift instead of fixing it.\n continue;\n }\n\n addColumn(table, stamp.column, definition);\n if (exists && recorded === undefined) {\n searchAdopted.push({ table: table.qualified, column: stamp.column });\n }\n if (recorded !== stamp.fingerprint) {\n actions.push({ kind: \"comment-column\", target: `${table.qualified}.${stamp.column}`, sql: stamp.sql });\n }\n }\n }\n\n // 3b. A junction that already existed, but is short a column. One created\n // above already carries both, so re-listing them would log two no-op\n // statements and inflate the count of changes applied.\n for (const table of junctionTables) {\n if (created.has(table.qualified)) continue;\n for (const column of table.columns) {\n if (renameLegacyColumn(table, column)) continue;\n // A payload column (`through.properties`) goes through `planColumn`,\n // the same pass a collection's scalar columns take. The bare\n // `ADD COLUMN <type>` below is right for the two key columns — they\n // are NOT NULL by the composite primary key and have no default —\n // and wrong for everything else: it would drop a `defaultValue`, and\n // it would either omit a NOT NULL the collection declared or apply\n // one to a junction that already holds rows without a value to\n // backfill them. `planColumn` is the one place that decides which of\n // those is safe, and it reports the constraint it withheld.\n if (column.source.kind === \"property\") {\n planColumn(table, column);\n continue;\n }\n addColumn(table, column.column, renderPgType(column.type));\n }\n }\n\n // 3c. The columns relation and reference properties own.\n for (const table of collectionTables) {\n for (const column of table.columns) {\n if (column.source.kind !== \"relation\" && column.source.kind !== \"reference\") continue;\n // A declared property already emits this column (an explicit\n // `postId` beside the `belongsTo` that uses it); the relation\n // contributes only the constraint, below.\n if (column.columnOwnedByProperty) continue;\n if (renameLegacyColumn(table, column)) continue;\n planColumn(table, column);\n }\n }\n\n // 4. Foreign keys, last: the tables and columns on both ends have to exist\n // first, and a constraint is the one thing here that can fail on data\n // rather than on schema, so nothing else depends on it.\n const knownConstraints = existing.constraints ?? new Set<string>();\n const plannedConstraints = new Set<string>();\n for (const table of [...collectionTables, ...junctionTables]) {\n for (const column of table.columns) {\n const fk = column.foreignKey;\n if (!fk) continue;\n const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;\n if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;\n plannedConstraints.add(name);\n actions.push({ kind: \"add-constraint\", target: name, sql: `${fk.sql};` });\n }\n }\n\n // 5. Indexes, after everything — the column has to exist, and this is the\n // one step that runs against a populated table for real work.\n //\n // CONCURRENTLY: a plain CREATE INDEX takes a lock that blocks writes for\n // the duration of the build, which on a live table is an outage. Each\n // statement here is issued on its own, outside any transaction, which is\n // the condition CONCURRENTLY requires.\n //\n // ...and only on a live table. It is withheld for a table this same plan\n // is creating: there are no rows to build from and no writer to block, so\n // the lock CONCURRENTLY avoids is a lock nobody would have taken. Worth\n // withholding rather than merely harmless, because CONCURRENTLY is the\n // one index form Postgres refuses inside a transaction block.\n const concurrent = (schema: string, table: string): boolean => !created.has(`${schema}.${table}`);\n for (const table of collectionTables) {\n if (!table.search) continue;\n for (const statement of searchIndexStatements(table.search)) {\n actions.push({\n kind: \"create-index\",\n target: table.qualified,\n sql: concurrent(table.schema, table.table)\n ? statement.replace(\"CREATE INDEX IF NOT EXISTS\", \"CREATE INDEX CONCURRENTLY IF NOT EXISTS\")\n : statement\n });\n }\n }\n\n // ANN indexes for vector columns, on the same terms. A column too wide\n // for pgvector to index is reported, not planned — silence there would\n // read as \"indexed\" to anyone watching the boot.\n const vectorIndexSkipped: SkippedVectorIndex[] = [];\n for (const table of collectionTables) {\n if (!table.vector) continue;\n for (const spec of table.vector.specs) {\n actions.push({\n kind: \"create-index\",\n target: `${spec.schema}.${spec.table}`,\n sql: concurrent(spec.schema, spec.table)\n ? vectorIndexStatement(spec).replace(\"CREATE INDEX IF NOT EXISTS\", \"CREATE INDEX CONCURRENTLY IF NOT EXISTS\")\n : vectorIndexStatement(spec)\n });\n }\n vectorIndexSkipped.push(...table.vector.skipped);\n }\n\n // Declared indexes, on exactly the same terms. Boot has to emit these,\n // not just `db push`: the managed runtime provisions at boot and never\n // runs a push, so a push-only index would simply not exist there — and\n // nothing would say so. `contracts/derived-names.txt` states the rule\n // (\"Both, or it is a bug\") and the gate enforces it.\n //\n // `concurrently` is a parameter rather than a string replacement on the\n // rendered SQL: the `.replace(…)` above silently does nothing for a\n // UNIQUE index, whose text is `CREATE UNIQUE INDEX …`.\n for (const spec of sortIndexSpecs(collectionTables.flatMap(t => t.indexes))) {\n actions.push({\n kind: \"create-index\",\n target: `${spec.schema}.${spec.table}`,\n sql: collectionIndexStatement(spec, {\n concurrently: concurrent(spec.schema, spec.table),\n ifNotExists: true\n })\n });\n }\n\n // 6. `autoValue: \"on_update\"` triggers. After the column, and idempotent by\n // DROP-then-CREATE rather than `CREATE OR REPLACE TRIGGER`, which exists\n // only on Postgres 14+.\n for (const table of collectionTables) {\n for (const trigger of table.triggers) {\n // Named after its table and column, so a trigger already in the\n // catalogue is this one — re-issuing it every boot would work and\n // would report a change on a database that has none.\n if (existing.triggers?.has(`${table.qualified}.${trigger.name}`)) continue;\n for (const statement of triggerStatements(trigger)) {\n actions.push({\n kind: \"create-trigger\",\n target: `${table.qualified}.${trigger.name}`,\n sql: statement\n });\n }\n }\n }\n\n // A column the database still requires that no property declares any more.\n //\n // Computed last, over the same live snapshot the rest of the plan read, and\n // only for tables this run did not create — a table created here has exactly\n // the columns the collection asked for, so there is nothing to orphan.\n //\n // Every element of the test matters. NOT NULL, because a nullable leftover\n // accepts a write and is merely untidy. No DEFAULT, because a leftover with\n // one is filled in for you. Not declared, because that is what makes it\n // unreachable — the author has no field to send.\n const orphanedRequiredColumns: OrphanedRequiredColumn[] = [];\n if (existing.notNullColumns && existing.columnDefaults) {\n for (const table of collectionTables) {\n const live = existing.tables.get(table.qualified);\n if (!live || created.has(table.qualified)) continue;\n const declared = new Set<string>();\n for (const column of table.columns) {\n declared.add(column.column);\n if (column.legacyColumn) declared.add(column.legacyColumn);\n }\n for (const column of live) {\n const key = `${table.qualified}.${column}`;\n if (declared.has(column)) continue;\n if (!existing.notNullColumns.has(key)) continue;\n if (existing.columnDefaults.has(key)) continue;\n orphanedRequiredColumns.push({ table: table.qualified, column, slug: table.slug! });\n }\n }\n }\n\n return {\n actions,\n statements: actions.map(a => a.sql),\n legacyForeignKeys,\n searchDrift,\n searchAdopted,\n vectorIndexSkipped,\n withheldConstraints,\n columnTypeDrift,\n orphanedRequiredColumns\n };\n}\n\n/** What a `CREATE FUNCTION` statement is called, for the action log. */\nconst functionTarget = (statement: string): string => {\n if (statement.includes(\"set_updated_at\")) return SET_UPDATED_AT_FN;\n return statement.includes(\"unaccent\") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN;\n};\n","/**\n * Applying a schema plan to a database that is already running.\n *\n * ## What this module is\n *\n * A managed runtime boots someone else's compiled project against a database it\n * has never seen. Auth tables are ensured at boot already, but collection tables\n * were not created by anything: the platform ran the app and every `/api/data/*`\n * request answered 500 on a missing relation. `rebase db push` cannot help — it\n * is an Atlas-driven CLI command, and the runtime image ships no CLI.\n *\n * Two halves, and only one of them is here. *What the collections ask for* is\n * `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle\n * file; *what the database already has, and which of those statements are safe\n * to run against it* is `schema/plan/diff-plan.ts`. This module reads the\n * catalogue, runs the statements, and reports what happened.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, against a database with customers' data in it, with no\n * human reading a diff. So it may only ever do things that cannot lose data.\n * See `diff-plan.ts` for the whole of that argument.\n */\nimport {\n declaredDatabaseExtensions,\n REBASE_SCHEMA,\n type CollectionConfig\n} from \"@rebasepro/types\";\nimport { relationalCollections } from \"@rebasepro/common\";\nimport { logger, isConcurrentDdlRace, isDuplicateObjectRace } from \"@rebasepro/server\";\nimport { SEARCH_STAMP_PREFIX } from \"./search-column\";\nimport { vectorExtensionHint } from \"./vector-index\";\nimport { planJunctionTables, quoteSqlLiteral } from \"./generate-postgres-ddl-logic\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport {\n assertSafeIdentifier,\n diffPlanAgainstCatalogue,\n type ConstraintPolicy,\n type DiffOptions,\n type EnsureAction,\n type EnsurePlan,\n type ExistingSchema,\n type LegacyForeignKey,\n type OrphanedRequiredColumn,\n type SearchColumnDrift,\n type WithheldConstraint\n} from \"./plan/diff-plan\";\nimport { extractCauseMessage, extractPgError } from \"../utils/pg-error-utils\";\nimport { columnTypeDriftMessage, type ColumnTypeDrift } from \"./column-type-drift\";\n\nexport type {\n ConstraintPolicy,\n EnsureAction,\n EnsurePlan,\n ExistingSchema,\n LegacyForeignKey,\n OrphanedRequiredColumn,\n SearchColumnDrift,\n WithheldConstraint\n};\n\n/**\n * The subset of a database handle this needs: run a statement, get rows back.\n *\n * Deliberately parameterless. Everything here is DDL or catalogue reads keyed by\n * schema name, and schema names are identifiers — they cannot be bound as\n * parameters anyway. They are validated by `assertSafeIdentifier` before they\n * reach a statement, so a config that somehow carried a quote is refused rather\n * than concatenated.\n */\nexport interface Queryable {\n query<T = unknown>(sql: string): Promise<{ rows: T[] }>;\n}\n\nexport interface EnsureOptions extends DiffOptions {\n /**\n * Server extensions the project's databases gave Rebase leave to install —\n * `declaredDatabaseExtensions()`. Absent means none, which is a refusal and\n * is the right default for a planner given no configuration at all.\n *\n * Explicit rather than read from the resource registry in here, because the\n * planner is pure and several callers plan against fixtures: reaching for a\n * process-wide registry would make the plan depend on whatever some other\n * module happened to import. `ensureCollectionTables` is the boundary that\n * reads the world.\n */\n databaseExtensions?: readonly string[];\n}\n\nexport interface EnsureOutcome extends EnsurePlan {\n /**\n * Actions that could not be applied and are non-fatal by nature.\n *\n * Two kinds qualify. A foreign key can only fail on data that already\n * violates it, and the column it would police exists either way, so the\n * collection still serves; refusing to boot over one would turn a\n * pre-existing data problem into an outage. A column comment is the search\n * fingerprint, which needs table ownership — losing it costs drift\n * detection on the next boot, not the deployment. Both are reported loudly.\n */\n failures: { kind: EnsureAction[\"kind\"]; target: string; error: string }[];\n}\n\n/** The schema a collection lives in, validated before it reaches any DDL. */\nfunction schemaOf(collection: CollectionConfig): string {\n const schema = (collection as { schema?: string }).schema || \"public\";\n return assertSafeIdentifier(schema, \"schema name\");\n}\n\n/**\n * Decide what to add. Pure — the caller supplies what exists and runs the\n * result.\n *\n * A plan and a diff: the collections are read once, by `planSchema`, into the\n * same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff\n * subtracts what the database already has. Before that split this function had\n * its own reading of every `Property`, and the audit found twelve places where\n * it disagreed with the two generators — a required `author_id` that was\n * NOT NULL after a push and nullable after a boot among them, on the one path\n * with no developer in the loop.\n */\nexport function planCollectionSchemaEnsure(\n allCollections: CollectionConfig[],\n existing: ExistingSchema,\n options: EnsureOptions = {}\n): EnsurePlan {\n const plan = planSchema(allCollections, { databaseExtensions: options.databaseExtensions });\n return diffPlanAgainstCatalogue(plan, existing, { constraints: options.constraints });\n}\n\n/** Read what the database has, for the schemas the collections live in. */\nexport async function readExistingSchema(\n client: Queryable,\n schemas: string[]\n): Promise<ExistingSchema> {\n const tables = new Map<string, Set<string>>();\n const enums = new Set<string>();\n if (schemas.length === 0) return { tables, enums };\n\n const inList = schemas\n .map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`)\n .join(\", \");\n\n const notNullColumns = new Set<string>();\n // `udt_name` rather than `data_type`: `data_type` collapses every array to\n // the literal string `ARRAY` and every extension type to `USER-DEFINED`,\n // which cannot be compared with anything. `udt_name` names the actual type\n // — `int4`, `jsonb`, `_numeric`, `vector` — which is what the drift check\n // needs.\n const columnTypes = new Map<string, string>();\n // A NOT NULL column that has a DEFAULT is not a problem for a write that\n // omits it, so orphan detection needs the default as well as the nullability\n // — otherwise every `created_at DEFAULT now()` on a table whose collection\n // does not declare it would be reported as a blocker.\n const columnDefaults = new Set<string>();\n const { rows: columns } = await client.query<{\n table_schema: string;\n table_name: string;\n column_name: string;\n is_nullable: string;\n udt_name: string | null;\n column_default: string | null;\n numeric_precision: number | null;\n numeric_scale: number | null;\n }>(\n // `numeric_precision`/`numeric_scale` because `udt_name` is `numeric`\n // for both `NUMERIC` and `NUMERIC(10, 2)`, and a property that declares\n // a precision means it: money stored in an unbounded column keeps the\n // third decimal the rounding was supposed to remove. Only carried for\n // `numeric`, where the modifier changes what a value *is*; a `varchar`\n // width is a limit on the same family and `typesAgree` ignores it.\n `SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,\n numeric_precision, numeric_scale\n FROM information_schema.columns\n WHERE table_schema IN (${inList})`\n );\n for (const row of columns) {\n const key = `${row.table_schema}.${row.table_name}`;\n if (!tables.has(key)) tables.set(key, new Set());\n tables.get(key)!.add(row.column_name);\n if (row.is_nullable === \"NO\") notNullColumns.add(`${key}.${row.column_name}`);\n if (row.udt_name) {\n const modifier = row.udt_name === \"numeric\" && row.numeric_precision !== null\n ? `(${row.numeric_precision},${row.numeric_scale ?? 0})`\n : \"\";\n columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);\n }\n if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);\n }\n\n // Trigger names, so `autoValue: \"on_update\"` is installed once rather than\n // re-issued on every boot. `tgisinternal` excludes the ones Postgres itself\n // creates to enforce foreign keys.\n const triggers = new Set<string>();\n const { rows: triggerRows } = await client.query<{ schema: string; table: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name\n FROM pg_trigger t\n JOIN pg_class c ON t.tgrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`\n );\n for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Which tables hold rows. This is the only fact that decides whether a\n // NOT NULL can be added without reading the data, so it is worth a query.\n //\n // `reltuples` would be cheaper and is wrong for this: it is a planner\n // estimate, it is -1 on a table that has never been analyzed, and a table\n // that was full an hour ago still reads as full after a DELETE. A wrong\n // \"empty\" here means a boot that aborts on a constraint violation, so the\n // estimate is not good enough. `EXISTS … LIMIT 1` stops at the first row,\n // which makes the true cost one page read per table.\n //\n // Restricted to ordinary and partitioned tables: `information_schema.columns`\n // also lists views and materialized views, and probing those runs whatever\n // query defines them.\n const populatedTables = new Set<string>();\n const { rows: realTables } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS name\n FROM pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE c.relkind IN ('r', 'p') AND n.nspname IN (${inList})`\n );\n if (realTables.length > 0) {\n const probes = realTables.map(row => {\n const schema = assertSafeIdentifier(row.schema, \"schema name\");\n const table = assertSafeIdentifier(row.name, \"table name\");\n return `SELECT ${quoteSqlLiteral(`${schema}.${table}`)} AS key, ` +\n `EXISTS(SELECT 1 FROM \"${schema}\".\"${table}\" LIMIT 1) AS populated`;\n });\n const { rows: populationRows } = await client.query<{ key: string; populated: boolean }>(\n probes.join(\" UNION ALL \")\n );\n for (const row of populationRows) {\n if (row.populated) populatedTables.add(row.key);\n }\n }\n\n const enumValues = new Map<string, string[]>();\n const { rows: enumValueRows } = await client.query<{\n schema: string;\n name: string;\n value: string;\n }>(\n // Ordered by `enumsortorder`, not by label: an enum's order is part of\n // its meaning (it is what `<` compares), and reading it back sorted\n // alphabetically would make a correct type look drifted.\n `SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value\n FROM pg_enum e\n JOIN pg_type t ON e.enumtypid = t.oid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname IN (${inList})\n ORDER BY t.typname, e.enumsortorder`\n );\n for (const row of enumValueRows) {\n const key = `${row.schema}.${row.name}`;\n if (!enumValues.has(key)) enumValues.set(key, []);\n enumValues.get(key)!.push(row.value);\n }\n\n const { rows: enumRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, t.typname AS name\n FROM pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE t.typtype = 'e' AND n.nspname IN (${inList})`\n );\n for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);\n\n // `ADD CONSTRAINT` has no IF NOT EXISTS, so an existing foreign key is\n // skipped by name rather than guarded in SQL.\n const constraints = new Set<string>();\n const { rows: constraintRows } = await client.query<{\n schema: string;\n table: string;\n name: string;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, con.conname AS name\n FROM pg_constraint con\n JOIN pg_class c ON con.conrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname IN (${inList})`\n );\n for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Column comments, which is where a generated search column records the\n // expression it was built from. `objsubid > 0` is what makes a row a\n // *column* comment rather than the table's own.\n const columnComments = new Map<string, string>();\n const { rows: commentRows } = await client.query<{\n schema: string;\n table: string;\n column: string;\n comment: string | null;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment\n FROM pg_description d\n JOIN pg_class c ON d.objoid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid\n WHERE d.objsubid > 0 AND n.nspname IN (${inList})`\n );\n for (const row of commentRows) {\n if (row.comment == null) continue;\n columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);\n }\n\n return {\n tables, enums, constraints, columnComments, enumValues, notNullColumns, populatedTables, columnTypes,\n columnDefaults, triggers\n };\n}\n\n/**\n * What to tell an operator whose `search` block no longer matches its column.\n *\n * Every line here is doing work: naming the collection is not enough, because\n * the symptom (a search that finds nothing) points at the data, not the schema;\n * and the remediation has to be exact, because it is a table rewrite the\n * operator is being asked to schedule rather than discover.\n */\nfunction searchDriftMessage(drift: SearchColumnDrift[]): string {\n const blocks = drift.map(d =>\n ` \"${d.table}\".\"${d.column}\" was generated from a different \\`search\\` block ` +\n `(recorded ${d.found}, current ${d.expected}).\\n` +\n d.rebuild.map(s => ` ${s}`).join(\"\\n\")\n );\n return (\n \"The `search` block changed after its generated column was created, and Postgres cannot alter a \" +\n \"generated expression in place.\\n\" +\n \"Rebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole \" +\n \"table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path \" +\n \"may not schedule on your behalf.\\n\" +\n \"Until it is rebuilt the column keeps indexing the previous fields, weights and language — searches for \" +\n \"anything added since return nothing, which reads from outside as \\\"no such row\\\".\\n\" +\n \"Run these (or revert the block to what the column was built from), then boot again:\\n\" +\n blocks.join(\"\\n\") +\n \"\\n The GIN index is dropped with the column and recreated concurrently on the next boot.\"\n );\n}\n\n\n/**\n * Read what the database looks like, for the schemas a set of collections\n * lives in.\n *\n * The same read `ensureCollectionTables` does at boot, exposed on its own for\n * the callers that want to *plan* against a real database without changing it —\n * the live schema editor, which has to tell somebody what a change would do\n * before they agree to it.\n */\nexport async function readSchemaFactsFor(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<ExistingSchema> {\n const relational = relationalCollections(collections);\n const schemas = Array.from(new Set([\n ...relational.map(schemaOf),\n ...planJunctionTables(relational).map(junction => junction.schema)\n ]));\n return readExistingSchema(client, schemas);\n}\n\n/**\n * Bring the database up to date. Returns what it did.\n *\n * Each statement runs on its own rather than in one transaction: they are all\n * independently safe and idempotent, and a single failure (an enum label that\n * cannot be added, say) should not roll back the tables that were created fine.\n * The error is surfaced with the statement that caused it.\n */\nexport async function ensureCollectionTables(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void,\n options: EnsureOptions = {}\n): Promise<EnsureOutcome> {\n // This is the boundary, so this is where the world is read: the planner\n // itself takes the permission as an argument and never reaches for the\n // registry. By the time boot gets here `loadBundleResourceGraph` has\n // evaluated the project's `resources.ts`, so the declarations are there —\n // and a caller that already knows them can still say so.\n const schema = planSchema(collections, {\n databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions()\n });\n\n // Junctions live alongside the collections that declare them, so their\n // schema has to be read too — otherwise an existing junction reads as\n // missing and its constraints as unplanned. Taken off the plan, which lists\n // both kinds of table.\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n for (const name of schemas) {\n assertSafeIdentifier(name, \"schema name\");\n if (name !== \"public\") {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${name}\";`);\n }\n }\n // `rebase` is where the `updated_at` trigger's function lives. Every real\n // boot has the schema by the time this runs (auth ensures it first) and\n // `db push` creates it in `schema.sql`, but this path is also driven by the\n // live schema editor and by tests, where \"the function's schema happens to\n // exist already\" is not a thing to rely on. Only when something needs it,\n // so a project with no trigger issues no statement — and not added to the\n // read set: what is *in* `rebase` is auth's business, not this planner's.\n if (schema.tables.some(table => table.triggers.length > 0)) {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${REBASE_SCHEMA}\";`);\n }\n\n const existing = await readExistingSchema(client, schemas);\n const plan = diffPlanAgainstCatalogue(schema, existing, { constraints: options.constraints });\n const failures: EnsureOutcome[\"failures\"] = [];\n\n // Reported, not warned: this is a rename the ensure is about to perform, and\n // the operator should be able to see in the log why a column changed name.\n // The interesting case is the one that no longer happens — before this,\n // ensure added the new column empty beside the populated old one, every\n // statement succeeded, and the relation read the empty one.\n for (const legacy of plan.legacyForeignKeys) {\n const message =\n `Renaming \"${legacy.table}\".\"${legacy.legacy}\" to \"${legacy.expected}\". The old name is ` +\n \"the one Rebase derived for this relation before it singularized properly; the column \" +\n \"keeps its data, indexes and constraints. To keep the old name instead, set \" +\n `\\`localKey: \"${legacy.legacy}\"\\` on the relation and this will stop.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Before anything is applied: a `search` block that changed after its column\n // was generated cannot be honoured by an additive plan, and serving the old\n // index while the config describes a new one is the silent failure this\n // check exists to end. Refusing is the loud half — boot is fatal on purpose\n // (see `ensureCollectionSchema` in the server's boot) and the message\n // carries the exact statements that resolve it.\n if (plan.searchDrift.length > 0) {\n throw new Error(searchDriftMessage(plan.searchDrift));\n }\n\n // Said once per column, at the moment the stamp is applied: from here on a\n // change is detected, but whether *this* column matches the block it is\n // being stamped with is not knowable — it predates the stamp.\n for (const adopted of plan.searchAdopted) {\n const message =\n `Adopting the existing generated column \"${adopted.table}\".\"${adopted.column}\" and recording what the ` +\n \"current `search` block would generate. Any later change to that block will be detected and refused; a \" +\n \"change made *before* this version was deployed cannot be, so if search has been missing content, \" +\n `rebuild the column once: ALTER TABLE \"${adopted.table.split(\".\").join('\".\"')}\" DROP COLUMN \"${adopted.column}\"; and boot again.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot: an unindexed vector column and an\n // indexed one behave identically apart from latency, so the only way anyone\n // learns which one they have is if the boot says so.\n for (const skip of plan.vectorIndexSkipped) {\n const message = `No ANN index on \"${skip.table}\".\"${skip.column}\": ${skip.reason}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, because the alternative is what this\n // whole feature exists to end: a column the configuration calls required,\n // sitting there nullable, with every surface reporting success. The boot\n // does not fail over it — the column is usable and the data is intact — but\n // it stops being invisible.\n for (const withheld of plan.withheldConstraints) {\n const message =\n `No NOT NULL on \"${withheld.target}\": ${withheld.reason} ${withheld.remedy}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per drifted column, every boot, and this is the one report here\n // that describes a system which currently *looks* healthy. Everything else\n // in this function explains a statement that did not run; this explains a\n // statement that was never planned, on a deploy that reported success, and\n // whose first symptom is a write rejected long afterwards.\n for (const drift of plan.columnTypeDrift) {\n const message = columnTypeDriftMessage(drift);\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, and it matters more than the rest here\n // because the table is not merely imperfect — it is unwritable, and the\n // error the API returns names a field the author has already deleted.\n // This is what a renamed property looks like to an additive provisioner.\n for (const orphan of plan.orphanedRequiredColumns) {\n const [schema, table] = orphan.table.split(\".\");\n const message =\n `\"${orphan.table}\".\"${orphan.column}\" is NOT NULL with no default, and the \"${orphan.slug}\" ` +\n \"collection no longer declares it. Every insert will be rejected for a field you cannot set \" +\n `(PG_23502, naming \"${orphan.column}\"). This is what a renamed property looks like: the new ` +\n \"column was added, and the old one cannot be dropped unattended. Fix it with ONE of:\\n\" +\n ` ALTER TABLE \"${schema}\".\"${table}\" DROP COLUMN \"${orphan.column}\"; -- the rename is complete\\n` +\n ` ALTER TABLE \"${schema}\".\"${table}\" ALTER COLUMN \"${orphan.column}\" DROP NOT NULL; -- keep the data, stop requiring it\\n` +\n \" ...or declare the property again, if the column is still wanted.\";\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n if (plan.actions.length === 0) {\n log?.(\"Schema is up to date; nothing to create.\");\n return { ...plan, failures };\n }\n\n for (const action of plan.actions) {\n try {\n if (await applyAction(client, action)) {\n log?.(`${action.kind}: ${action.target}`);\n } else {\n log?.(`${action.kind}: ${action.target} (already created by a peer)`);\n }\n } catch (err) {\n const message = describeDriverError(err);\n // A foreign key is the only action that can fail on the customer's\n // data rather than on the schema. The column it polices is already\n // there, so the collection serves either way — record it and carry\n // on rather than crash-looping the deployment.\n //\n // A comment is metadata about a column that was just created\n // successfully, and it can only fail on ownership (COMMENT requires\n // owning the table, which an adopted table may not grant). Losing\n // the stamp costs drift detection on the next boot; it must not cost\n // the deployment.\n //\n // An index that is not UNIQUE is an optimization, and losing an\n // optimization must not cost the deployment. This was the difference\n // between a slow collection and a tenant that would not boot: on a\n // managed runtime the throw killed the process, the pod never went\n // ready, and the deploy reported a 300-second readiness timeout — for\n // an ANN index whose absence changes latency and nothing else.\n //\n // UNIQUE is excluded because it is not an optimization. It is the\n // collection declaring that two rows cannot share a value, and\n // serving without it is serving a promise the database is not\n // keeping.\n const survivable =\n action.kind === \"add-constraint\" ||\n action.kind === \"comment-column\" ||\n (action.kind === \"create-index\" && !/CREATE\\s+UNIQUE\\s+INDEX/i.test(action.sql));\n if (survivable) {\n failures.push({ kind: action.kind, target: action.target, error: message });\n continue;\n }\n throw new Error(\n `Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\\n ${action.sql}`\n );\n }\n }\n return { ...plan, failures };\n}\n\n/**\n * What actually went wrong, rather than what the ORM said about it.\n *\n * Drizzle wraps every driver error, and its `message` is literally\n * ``Failed query: ${sql}\\nparams: ${params}`` — the statement we already know,\n * and not one word of Postgres's answer. Reporting `err.message` therefore\n * produced a failure that named the statement twice and the *reason* zero\n * times, so four unrelated boot failures — a missing extension, a wrong\n * privilege, a bad index method, a constraint the rows violate — all read\n * identically to whoever had to fix them.\n *\n * Two consequences, both observed in the field before this existed:\n *\n * 1. On a managed deployment the reason never reaches anybody. Boot dies, the\n * pod never goes ready, and the deploy reports a readiness timeout — so the\n * one line that says why is the one line that was thrown away here.\n * 2. {@link vectorExtensionHint} matches on the error text. Against the\n * wrapper it matched nothing, so the hint written specifically to explain a\n * missing pgvector never fired on the path that raises it.\n *\n * `detail` and `hint` come along because they are the fields Postgres uses to\n * say which object it meant and what to do about it — `CREATE EXTENSION` names\n * the missing library there, and a rejected constraint names the row. This\n * writes into the tenant's own boot log, about the tenant's own database.\n */\nexport function describeDriverError(err: unknown): string {\n const pg = extractPgError(err);\n if (pg) {\n const parts = [pg.code ? `${pg.message} [${pg.code}]` : pg.message];\n if (pg.detail) parts.push(` detail: ${pg.detail}`);\n if (pg.hint) parts.push(` hint: ${pg.hint}`);\n return parts.join(\"\\n\");\n }\n // No SQLSTATE anywhere in the chain: not a database answer at all — a\n // socket that closed, a bug in this file. The deepest cause still beats the\n // wrapper, and the wrapper still beats nothing.\n const cause = extractCauseMessage(err);\n if (cause) return cause;\n return err instanceof Error ? err.message : String(err);\n}\n\n/** ` CONCURRENTLY ` as it appears in a rendered `CREATE [UNIQUE] INDEX`. */\nconst CONCURRENTLY_RE = /\\sCONCURRENTLY\\s/i;\n\n/**\n * `25001 active_sql_transaction` — \"cannot run inside a transaction block\".\n *\n * Only ever raised here by `CREATE INDEX CONCURRENTLY`, and only when the\n * connection we were handed is inside a transaction. Deliberately narrow: it\n * decides to retry a *different* statement, so it must not fire on anything it\n * has not been reasoned about.\n */\nfunction isTransactionBlockRefusal(err: unknown): boolean {\n return extractPgError(err)?.code === \"25001\";\n}\n\n/** Attempts per action, including the first. Matches the server's bootstraps. */\nconst DDL_ATTEMPTS = 4;\n\n/**\n * Run one planned statement, surviving a simultaneous boot.\n *\n * Every statement in a plan is written to be idempotent, and that is not the\n * same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the\n * catalog and then writes to it as two steps, so peers starting together both\n * see \"absent\" and the loser gets a duplicate key on a *catalog* index. Measured\n * against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is\n * worse, because Postgres has no `IF NOT EXISTS` for it at all.\n *\n * What made that fatal here rather than merely noisy is the loop this sits in.\n * A losing statement threw, and the throw abandoned **every remaining action in\n * the plan** — so a replica that lost one race came up missing tables it never\n * attempted, and the boot log blamed the one statement that failed.\n *\n * @returns `true` if this process applied the statement, `false` if a peer had\n * already created the object. The distinction is only for the log; both mean\n * the object is now there.\n * @throws the original error for anything that is not a race — a syntax error, a\n * permission failure, a unique constraint the customer's own rows violate.\n */\nasync function applyAction(\n client: Queryable,\n action: EnsureAction\n): Promise<boolean> {\n let sqlText = action.sql;\n for (let attempt = 1; ; attempt++) {\n try {\n await client.query(sqlText);\n return true;\n } catch (err) {\n // `CREATE INDEX CONCURRENTLY` is the one statement here Postgres\n // refuses inside a transaction block (25001), and whether we are in\n // one is not ours to know: the client is whatever handle the caller\n // bootstrapped with, and an application that builds its own adapter\n // can hand us a connection already inside a transaction. The plain\n // form is always correct and never refused — it takes a write lock\n // for the build, which is the cost of getting the index at all.\n if (isTransactionBlockRefusal(err) && CONCURRENTLY_RE.test(sqlText)) {\n sqlText = sqlText.replace(CONCURRENTLY_RE, \" \");\n logger.warn(\n `[schema] ${action.kind} ${action.target}: this connection is inside a transaction, ` +\n \"so the index is being built with a write lock rather than concurrently. Writes to \" +\n \"this table block until it finishes.\"\n );\n continue;\n }\n // Already there. Not \"retry\" — the end state this statement wanted\n // is the end state the database is in, so carry on to the next\n // action rather than spending three more attempts proving it.\n if (isDuplicateObjectRace(err)) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: already created by another instance`\n );\n return false;\n }\n // Retryable but not yet satisfied — a deadlock between two boots\n // taking catalog locks in step. The statement did nothing; run it\n // again after a jittered pause so peers that collided once do not\n // collide again in lockstep.\n if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: lost a race with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await new Promise(resolve => setTimeout(resolve, 40 * attempt * (1 + Math.random())));\n continue;\n }\n throw err;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"introspect-db-logic-C6LQdTxj.js","names":[],"sources":["../src/schema/introspect-db-naming.ts","../src/schema/introspect-db-inference.ts","../src/schema/introspect-db-types.ts","../src/schema/introspect-db-structure.ts","../src/schema/introspect-db-logic.ts"],"sourcesContent":["/**\n * Naming helpers shared by the introspection modules. These live apart from\n * `introspect-db-logic.ts` because the inference pass needs them too, and\n * importing them from there would close a cycle back through this module.\n */\n\n/**\n * Convert a snake_case name to a human-readable Title Case label.\n * e.g. \"created_at\" -> \"Created At\", \"customer_id\" -> \"Customer Id\"\n */\nexport function humanize(snakeName: string): string {\n return snakeName\n .replace(/_/g, \" \")\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n","import { humanize } from \"./introspect-db-naming\";\n\nexport interface InferenceResult {\n propType?: string; // If the inference changes the base type\n extra?: string; // String to append to the property definition\n}\n\nconst ISO_8601_REGEX = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/;\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst CUID_REGEX = /^c[^\\s-]{7,}$/i; // Basic CUID check\nconst COLOR_HEX_REGEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;\n\nexport function inferPropertyFromData(\n columnName: string,\n pgDataType: string,\n currentPropType: string,\n sampleValues: unknown[],\n isPk: boolean,\n /**\n * False when generating for a project without `@rebasepro/cms-types`, where\n * `BaseProperty` declares no `admin` field and the block below would not\n * compile. The type-level inferences (`propType`, `url`, `storage`) are\n * unaffected — only the form-widget hints are dropped.\n */\n emitAdmin = true\n): InferenceResult {\n const result: InferenceResult = {};\n const extraLines: string[] = [];\n // Collected separately and emitted as one block at the end. Pushed inline,\n // two admin-owned options from different branches produced two `admin: {`\n // blocks in one property — a duplicate key, which is also a compile error.\n const adminOptions: string[] = [];\n\n // Filter out null/undefined for analysis\n const validValues = sampleValues.filter(v => v !== null && v !== undefined && v !== \"\");\n\n if (validValues.length === 0) {\n return result; // Not enough data\n }\n\n const colNameLower = columnName.toLowerCase();\n\n // ── Number Analysis ──────────────────────────────────────────────────\n if (currentPropType === \"number\") {\n // Boolean stored as int\n const allZeroOrOne = validValues.every(v => v === 0 || v === 1 || v === \"0\" || v === \"1\");\n if (allZeroOrOne) {\n result.propType = \"boolean\";\n return result; // Don't do min/max if boolean\n }\n\n // Min / Max constraints\n const numValues = validValues.map(v => Number(v)).filter(v => !isNaN(v));\n if (numValues.length === validValues.length) {\n const min = Math.min(...numValues);\n const max = Math.max(...numValues);\n // Example heuristic: percentages\n if (min >= 0 && max <= 100 && (colNameLower.includes(\"percent\") || colNameLower.includes(\"rate\") || colNameLower.includes(\"score\"))) {\n extraLines.push(\" validation: {\\n min: 0,\\n max: 100\\n }\");\n } else if (min >= 0 && (colNameLower.includes(\"count\") || colNameLower.includes(\"total\") || colNameLower.includes(\"amount\"))) {\n extraLines.push(\" validation: {\\n min: 0\\n }\");\n }\n }\n\n // No currency heuristic. `admin.currency` was never a declared option and\n // nothing reads it, so a column called `price` got a block of config that did\n // nothing — invisible because this generator emits *source text*, which no\n // typechecker ever saw. Reinstate it by adding `currency` to\n // AdminNumberOptions first, and something that renders it.\n }\n\n // ── JSON / JSONB Analysis ────────────────────────────────────────────\n if (currentPropType === \"map\" || pgDataType.includes(\"json\")) {\n // PostGres jsonb can be object or array\n let allArrays = true;\n let allObjects = true;\n\n for (const v of validValues) {\n let parsed = v;\n if (typeof v === \"string\") {\n try { parsed = JSON.parse(v); } catch (e) { allArrays = false; allObjects = false; break; }\n }\n if (Array.isArray(parsed)) {\n allObjects = false;\n } else if (typeof parsed === \"object\" && parsed !== null) {\n allArrays = false;\n } else {\n allArrays = false;\n allObjects = false;\n }\n }\n\n if (allArrays && !allObjects) {\n result.propType = \"array\";\n // Infer inner type\n let allNumbers = true;\n let allStrings = true;\n for (const v of validValues) {\n const parsed = typeof v === \"string\" ? JSON.parse(v) : v;\n for (const item of parsed) {\n if (typeof item !== \"number\") allNumbers = false;\n if (typeof item !== \"string\") allStrings = false;\n }\n }\n const innerType = allNumbers ? \"number\" : allStrings ? \"string\" : \"map\";\n extraLines.push(` of: { name: \"${humanize(columnName)} Item\", type: \"${innerType}\" }`);\n } else {\n result.propType = \"map\";\n\n // Infer inner schema\n if (allObjects && validValues.length > 0) {\n const schema: Record<string, string> = {};\n for (const v of validValues) {\n const parsed = typeof v === \"string\" ? JSON.parse(v) : v;\n for (const [k, val] of Object.entries(parsed)) {\n if (val === null || val === undefined) continue;\n const type = typeof val;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n if (!schema[k]) schema[k] = type;\n else if (schema[k] !== type) schema[k] = \"mixed\";\n } else if (Array.isArray(val)) {\n if (!schema[k]) schema[k] = \"array\";\n else if (schema[k] !== \"array\") schema[k] = \"mixed\";\n } else if (type === \"object\") {\n if (!schema[k]) schema[k] = \"map\";\n else if (schema[k] !== \"map\") schema[k] = \"mixed\";\n }\n }\n }\n\n const keys = Object.keys(schema).filter(k => schema[k] !== \"mixed\");\n if (keys.length > 0) {\n const props = keys.map(k => {\n return `\\n ${k}: { name: \"${humanize(k)}\", type: \"${schema[k]}\" }`;\n }).join(\",\");\n extraLines.push(` properties: {${props}\\n }`);\n } else {\n extraLines.push(\" keyValue: true\");\n }\n } else {\n extraLines.push(\" keyValue: true\");\n }\n }\n }\n\n // ── String Analysis ──────────────────────────────────────────────────\n if (currentPropType === \"string\") {\n // Date/Time Strings\n if (validValues.every(v => typeof v === \"string\" && ISO_8601_REGEX.test(v))) {\n result.propType = \"date\";\n return result;\n }\n\n // Implicit Enums\n const uniqueValues = new Set(validValues);\n // Determine the maximum length among unique values\n let maxEnumLength = 0;\n for (const v of uniqueValues) {\n if (typeof v === \"string\" && v.length > maxEnumLength) {\n maxEnumLength = v.length;\n }\n }\n\n // Ensure no empty string, max length makes sense, and fewer unique values than total values (unless small total)\n if (uniqueValues.size > 0 && uniqueValues.size <= 5 && maxEnumLength <= 50 && validValues.length > uniqueValues.size && !uniqueValues.has(\"\")) {\n const isLikelyId = isPk || colNameLower.endsWith(\"_id\");\n if (!isLikelyId) {\n const enumEntries = Array.from(uniqueValues).map(v => `{ id: ${JSON.stringify(v)}, label: ${JSON.stringify(humanize(v as string))} }`).join(\", \");\n extraLines.push(` enum: [${enumEntries}]`);\n result.extra = extraLines.length > 0 ? \"\\n\" + extraLines.join(\",\\n\") + \",\" : \"\";\n return result; // Skip other string checks if it's an enum\n }\n }\n\n // UUID / CUID Detection\n const allUuid = validValues.every(v => typeof v === \"string\" && UUID_REGEX.test(v));\n const allCuid = validValues.every(v => typeof v === \"string\" && CUID_REGEX.test(v));\n if (allUuid) {\n if (isPk) extraLines.push(\" isId: \\\"uuid\\\"\");\n } else if (allCuid) {\n if (isPk) extraLines.push(\" isId: \\\"cuid\\\"\");\n }\n\n // No colour heuristic. `admin.color` was never a declared option and nothing\n // reads it — the same dead branch `currency` was. This generator emits *source\n // text*, so no typechecker ever saw either. Reinstate by adding the option and\n // something that renders it first.\n\n // Text Lengths, Multiline & Markdown\n let maxLength = 0;\n let hasNewlines = false;\n let hasMarkdown = false;\n for (const v of validValues) {\n if (typeof v === \"string\") {\n if (v.length > maxLength) maxLength = v.length;\n if (v.includes(\"\\n\")) hasNewlines = true;\n if (/^#{1,6}\\s+.+/m.test(v) || /\\*\\*.+\\*\\*/.test(v) || /\\[.+\\]\\(.+\\)/.test(v) || /^\\s*[-*]\\s+.+/m.test(v)) {\n hasMarkdown = true;\n }\n }\n }\n\n // `multiline` and `markdown` are *admin* property options, so they have\n // to sit inside the `admin` block. Emitted at the top of the property —\n // where they were — `PostgresCollectionConfig` does not declare them and\n // the generated file does not compile.\n if (hasMarkdown) {\n adminOptions.push(\"multiline: true\", \"markdown: true\");\n } else if (hasNewlines || maxLength > 100) {\n adminOptions.push(\"multiline: true\");\n }\n\n if (maxLength > 0 && maxLength < 10000) { // arbitrary cap to avoid huge limits\n // Pad the max length slightly for the constraint\n const paddedMax = Math.ceil((maxLength * 1.5) / 10) * 10;\n // Only add length constraint if it seems like a bounded string, not a long text\n if (paddedMax < 255) {\n extraLines.push(` validation: {\\n max: ${paddedMax}\\n }`);\n }\n }\n\n // Storage & Media (Extracted from old logic)\n const isUrl = colNameLower.endsWith(\"_url\") || colNameLower.endsWith(\"_uri\") || colNameLower.endsWith(\"_link\");\n const isMedia = colNameLower.includes(\"image\") || colNameLower.includes(\"avatar\") || colNameLower.includes(\"photo\") || colNameLower.includes(\"logo\") || colNameLower.includes(\"cover\");\n\n const allAbsoluteUrls = validValues.every(v => typeof v === \"string\" && (v.startsWith(\"http://\") || v.startsWith(\"https://\")));\n if (allAbsoluteUrls) {\n const isImage = validValues.some(v => typeof v === \"string\" && v.match(/\\.(jpeg|jpg|gif|png|webp|svg)/i));\n if (isImage || isMedia) {\n extraLines.push(\" url: true\");\n adminOptions.push(\"urlPreview: \\\"image\\\"\");\n } else {\n extraLines.push(\" url: true\");\n }\n } else {\n const hasFileExtension = validValues.some(v => typeof v === \"string\" && v.match(/\\.[a-zA-Z0-9]+$/));\n if (hasFileExtension) {\n const firstVal = validValues[0] as string;\n const lastSlash = firstVal.lastIndexOf(\"/\");\n const inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : \"files\";\n extraLines.push(` storage: {\\n storagePath: \"${inferredStoragePath}\"\\n }`);\n } else if (isUrl) {\n extraLines.push(\" url: true\");\n if (isMedia) adminOptions.push(\"urlPreview: \\\"image\\\"\");\n }\n }\n }\n\n if (emitAdmin && adminOptions.length > 0) {\n extraLines.push(` admin: {\\n${adminOptions.map((o) => ` ${o}`).join(\",\\n\")}\\n }`);\n }\n\n result.extra = extraLines.length > 0 ? \"\\n\" + extraLines.join(\",\\n\") + \",\" : \"\";\n return result;\n}\n","/**\n * The PostgreSQL type → Rebase property type mapping.\n *\n * Split out of `introspect-db-logic` so that the structural analysis can use it\n * without importing the generator, which imports the analysis. Re-exported from\n * `introspect-db-logic` so existing callers keep their import path.\n */\n\n/**\n * Map a PostgreSQL data type to a Rebase property type.\n */\nexport function mapPgType(dataType: string): string {\n const dt = dataType.toLowerCase();\n\n // Interval MUST be checked before numeric (\"interval\" contains \"int\")\n if (dt === \"interval\") return \"string\";\n\n // Array types MUST be checked before numeric (\"_int4\" contains \"int\")\n if (dt === \"array\" || dt.startsWith(\"_\")) return \"array\";\n\n // Numeric types\n if (\n dt.includes(\"int\") || // integer, smallint, bigint\n dt.includes(\"numeric\") ||\n dt.includes(\"decimal\") ||\n dt.includes(\"serial\") || // serial, bigserial\n dt === \"real\" ||\n dt === \"float4\" ||\n dt === \"float8\" ||\n dt === \"double precision\" ||\n dt === \"money\"\n ) {\n return \"number\";\n }\n\n // Boolean\n if (dt.includes(\"bool\")) return \"boolean\";\n\n // Date / Time\n if (dt.includes(\"time\") || dt.includes(\"date\")) return \"date\";\n\n // JSON\n if (dt === \"json\" || dt === \"jsonb\") return \"map\";\n\n // Binary\n if (dt === \"bytea\") return \"binary\";\n\n // Network types\n if (dt === \"inet\" || dt === \"cidr\" || dt === \"macaddr\" || dt === \"macaddr8\") return \"string\";\n\n // UUID\n if (dt === \"uuid\") return \"string\";\n\n // Text/varchar/char — default to string\n return \"string\";\n}\n","/**\n * What a schema's *structure* says about the app on top of it.\n *\n * Introspection has always been a table mirror: one table in, one collection\n * out, one nav entry each, every column a form field. A schema of thirty tables\n * produces thirty sidebar entries, and a panel whose navigation is a list of\n * table names reads as a database browser however good the fields are — which\n * is the actual complaint about generated admin panels, and is structural, not\n * cosmetic.\n *\n * Most of what separates the eight nouns a user navigates by from the thirty\n * tables underneath them is written down in the schema already: which tables\n * only exist to join two others, which are small referenced code lists, which\n * rows cannot outlive a parent row. This module reads that.\n *\n * ## Structure only\n *\n * Nothing here looks at a column or table *name*. Name heuristics — `status`,\n * `*_url`, `image`, `created_at` — are wrong exactly when a schema is not in\n * English, or is domain-specific, or spells things differently, and they are\n * wrong silently. Every rule below is a fact the database enforces: key\n * composition, foreign-key direction and delete rule, uniqueness, nullability,\n * declared type and length, generated-ness, row count.\n *\n * That constraint has a cost, and it is worth stating: a schema that declares\n * nothing beyond `NOT NULL` gives this module very little to work with, and it\n * returns `entity` for everything rather than guessing. Under-classifying is\n * the intended failure mode. A table wrongly hidden from the navigation is a\n * table the user cannot find; a table wrongly left in it is merely the status\n * quo.\n *\n * Pure module: no I/O. Row counts come in on {@link SchemaMetadata.rowCounts},\n * which the caller fills from {@link ./introspect-db-queries.countRowsUpTo} for\n * the tables {@link lookupCandidates} names.\n */\nimport type {\n ForeignKeyRow,\n SchemaMetadata,\n TableColumn,\n TableMeta,\n UniqueConstraintRow\n} from \"./introspect-db-logic\";\nimport { mapPgType } from \"./introspect-db-types\";\nimport type { CheckFactsByTable } from \"./introspect-db-constraints\";\n\n// ── Thresholds ────────────────────────────────────────────────────────\n\n/**\n * The row count above which a referenced table is a real entity rather than a\n * code list. Deliberately low: `pagila.category` has 16 rows and `language` 6,\n * while `actor` has 200 and `country` 109 — the gap between \"a fixed set\n * somebody typed once\" and \"data the app accumulates\" is wide, and picking a\n * number in the middle of it costs nothing.\n */\nexport const LOOKUP_MAX_ROWS = 50;\n\n/**\n * The most payload columns a code list may carry. A code list is an id, a\n * label, and perhaps a sort key or a flag; past that it is a table with\n * attributes, which is an entity.\n */\nexport const LOOKUP_MAX_PAYLOAD_COLUMNS = 3;\n\n/**\n * The most enum values a board can usefully have as columns. A kanban with\n * thirty columns is a horizontally scrolling table.\n */\nexport const KANBAN_MAX_VALUES = 12;\n\n/** Below this, a \"board\" is one or two columns — a filter, not a board. */\nexport const KANBAN_MIN_VALUES = 2;\n\n/**\n * How many columns a generated list view shows before it stops being readable.\n * Only applied when a table has more properties than this; a six-column table\n * gets no `listProperties` at all rather than a restatement of its own columns.\n */\nexport const LIST_PROPERTIES_CAP = 6;\n\n// ── Roles ─────────────────────────────────────────────────────────────\n\n/**\n * What a table *is*, structurally.\n *\n * - `entity` — a thing the app is about. Gets a collection and a nav entry.\n * - `junction` — exists only to relate two other tables. Gets no collection at\n * all; it becomes a many-to-many relation on both sides.\n * - `lookup` — a small, referenced, self-contained code list. Gets a collection,\n * grouped away from the entities rather than listed beside them.\n * - `owned-child` — rows that belong to exactly one parent row and are reached\n * through it. Gets a collection (it is a real table with real rows, and the\n * API still serves it) but no nav entry: it already renders as a tab on its\n * parent.\n */\nexport type TableRole = \"entity\" | \"junction\" | \"lookup\" | \"owned-child\";\n\n/**\n * Why a table was called someone's child, weakest last.\n *\n * Carried into the generated file as a comment. A reader who disagrees with the\n * classification needs to see what it was based on to know which line to change.\n */\nexport type OwnershipEvidence =\n /** The only foreign key declared `ON DELETE CASCADE`. */\n | \"cascade-delete\"\n /** The only foreign key that is part of the table's primary key. */\n | \"identifying-key\"\n /** The only foreign key that is `NOT NULL`. */\n | \"sole-required-key\"\n /** First column of a composite primary key made entirely of foreign keys. */\n | \"leading-key-column\";\n\nexport interface JunctionShape {\n sourceTable: string;\n sourceColumn: string;\n targetTable: string;\n targetColumn: string;\n}\n\nexport interface TableClassification {\n table: string;\n role: TableRole;\n /** One line, in prose, for the generated file. */\n reason: string;\n /** Set when `role === \"owned-child\"`. */\n owner?: { table: string; column: string; evidence: OwnershipEvidence };\n /** Set when `role === \"junction\"`. */\n junction?: JunctionShape;\n}\n\n// ── Column-level structural predicates ────────────────────────────────\n\n/**\n * A timestamp the database maintains: a temporal column defaulting to the\n * transaction clock.\n *\n * This is the structural stand-in for the `created_at`/`updated_at` name check.\n * It is strictly better than the name: it catches `fecha_creacion` and\n * `last_update` (pagila's spelling, which the name list misses), and it does not\n * fire on a user-editable `created_at date` column that has no default and which\n * the name check would wrongly make read-only.\n */\nexport function isAutoTimestamp(column: TableColumn): boolean {\n if (mapPgType(column.data_type) !== \"date\") return false;\n const columnDefault = (column.column_default ?? \"\").toLowerCase();\n if (!columnDefault) return false;\n return /\\b(now\\(\\)|current_timestamp|current_date|current_time|localtimestamp|localtime|transaction_timestamp\\(\\)|statement_timestamp\\(\\)|clock_timestamp\\(\\))/.test(columnDefault);\n}\n\n/** A key the database fills in: identity, serial, or a uuid-generating default. */\nexport function isGeneratedKey(column: TableColumn): boolean {\n if (column.is_identity === \"YES\") return true;\n const columnDefault = (column.column_default ?? \"\").toLowerCase();\n if (!columnDefault) return false;\n return columnDefault.includes(\"nextval(\") ||\n columnDefault.includes(\"gen_random_uuid\") ||\n columnDefault.includes(\"uuid_generate\");\n}\n\n/** A column Postgres computes; writing to it is an error. */\nexport function isGeneratedColumn(column: TableColumn): boolean {\n return column.is_generated === \"ALWAYS\";\n}\n\n/**\n * Types that exist to be searched or indexed, never to be typed into.\n *\n * A `tsvector` column is a derived search index — maintained by a trigger, a\n * generated expression, or an application job — and its contents are lexeme\n * positions, not text. Pagila's `film.fulltext` is one, and introspection used\n * to emit it as an ordinary required string: a mandatory form field whose\n * correct value no user can produce, on the sixth column of the list view.\n */\nexport function isDerivedIndexColumn(column: TableColumn): boolean {\n return column.udt_name === \"tsvector\" || column.udt_name === \"tsquery\";\n}\n\n/** Anything the user cannot meaningfully edit, whatever the reason. */\nexport function isReadOnlyColumn(column: TableColumn): boolean {\n return isGeneratedColumn(column) || isDerivedIndexColumn(column);\n}\n\n/**\n * A string column with a declared maximum length.\n *\n * `varchar(50)` and `text` are the same type to an application but not to the\n * author: choosing a bound is a statement that the value is short and\n * label-like, which is what makes this usable for picking a display column.\n */\nexport function isBoundedString(column: TableColumn): boolean {\n return mapPgType(column.data_type) === \"string\" &&\n typeof column.character_maximum_length === \"number\" &&\n column.character_maximum_length > 0;\n}\n\n/**\n * A column carrying data rather than structure: not a key, not a foreign key,\n * not a database-maintained timestamp, not computed.\n *\n * The count of these is what tells a pure join table from an association that\n * carries its own attributes — `northwind.order_details` has the key shape of a\n * junction and three payload columns, so it is not one.\n */\nexport function isPayloadColumn(column: TableColumn, pks: string[], fkColumns: Set<string>): boolean {\n if (pks.includes(column.column_name)) return false;\n if (fkColumns.has(column.column_name)) return false;\n if (isAutoTimestamp(column)) return false;\n if (isGeneratedColumn(column)) return false;\n return true;\n}\n\n// ── Foreign-key topology ──────────────────────────────────────────────\n\n/** One foreign key, with its columns grouped back together. */\nexport interface ForeignKeyConstraint {\n name: string;\n table: string;\n columns: string[];\n foreignTable: string;\n foreignColumns: string[];\n deleteRule?: string;\n}\n\n/**\n * Groups per-column foreign key rows back into constraints.\n *\n * Rows arrive one per referencing column. A composite key looks exactly like two\n * separate keys until they are grouped by constraint name, and the difference\n * matters: two single-column keys to two tables can be a junction, one\n * two-column key never is.\n */\nexport function groupForeignKeys(fks: ForeignKeyRow[]): ForeignKeyConstraint[] {\n const byName = new Map<string, ForeignKeyConstraint>();\n for (const fk of fks) {\n // Rows from older callers carry no constraint name. Falling back to a\n // per-column key treats each as its own single-column constraint, which\n // is what the generator assumed before names were available.\n const key = `${fk.table_name}::${fk.constraint_name ?? `${fk.column_name}->${fk.foreign_table_name}`}`;\n const existing = byName.get(key);\n if (existing) {\n existing.columns.push(fk.column_name);\n existing.foreignColumns.push(fk.foreign_column_name);\n continue;\n }\n byName.set(key, {\n name: fk.constraint_name ?? `${fk.table_name}_${fk.column_name}_fkey`,\n table: fk.table_name,\n columns: [fk.column_name],\n foreignTable: fk.foreign_table_name,\n foreignColumns: [fk.foreign_column_name],\n deleteRule: fk.delete_rule\n });\n }\n return Array.from(byName.values());\n}\n\n/** Column sets that are unique in a table: its primary key and every unique index. */\nfunction uniqueColumnSets(table: string, pks: string[], uniques: UniqueConstraintRow[]): string[][] {\n const sets: string[][] = [];\n if (pks.length > 0) sets.push(pks);\n for (const unique of uniques) {\n if (unique.table_name === table) sets.push(unique.column_names);\n }\n return sets;\n}\n\nfunction sameColumnSet(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false;\n const left = [...a].sort();\n const right = [...b].sort();\n return left.every((value, index) => value === right[index]);\n}\n\n// ── Classification ────────────────────────────────────────────────────\n\n/** Everything classification needs, assembled once per run. */\ninterface AnalysisContext {\n tables: Map<string, TableMeta>;\n constraintsByTable: Map<string, ForeignKeyConstraint[]>;\n inboundByTable: Map<string, ForeignKeyConstraint[]>;\n uniques: UniqueConstraintRow[];\n rowCounts: Record<string, number>;\n}\n\nfunction buildContext(metadata: SchemaMetadata, tables: Map<string, TableMeta>): AnalysisContext {\n const allConstraints = groupForeignKeys(metadata.fks);\n const constraintsByTable = new Map<string, ForeignKeyConstraint[]>();\n const inboundByTable = new Map<string, ForeignKeyConstraint[]>();\n\n for (const constraint of allConstraints) {\n const outbound = constraintsByTable.get(constraint.table) ?? [];\n outbound.push(constraint);\n constraintsByTable.set(constraint.table, outbound);\n\n const inbound = inboundByTable.get(constraint.foreignTable) ?? [];\n inbound.push(constraint);\n inboundByTable.set(constraint.foreignTable, inbound);\n }\n\n return {\n tables,\n constraintsByTable,\n inboundByTable,\n uniques: metadata.uniques,\n rowCounts: metadata.rowCounts\n };\n}\n\n/**\n * A table that exists only to relate two others.\n *\n * Requires all of:\n * - exactly two single-column foreign keys, to two distinct columns;\n * - that pair is unique — it is the primary key, or a unique index covers it.\n * Without this the table can hold the same pair twice, which is a list of\n * events between two things, not a set membership;\n * - no payload columns. A join row that carries a quantity is an association\n * with attributes, and collapsing it into a many-to-many silently drops those\n * attributes from the UI entirely;\n * - nothing references it. A junction with its own dependents is something rows\n * point *at*, so it needs an identity of its own.\n */\nfunction classifyJunction(table: string, context: AnalysisContext): TableClassification | null {\n const meta = context.tables.get(table);\n if (!meta) return null;\n\n const constraints = context.constraintsByTable.get(table) ?? [];\n if (constraints.length !== 2) return null;\n if (constraints.some((c) => c.columns.length !== 1)) return null;\n\n const [first, second] = constraints;\n if (first.columns[0] === second.columns[0]) return null;\n\n if ((context.inboundByTable.get(table) ?? []).length > 0) return null;\n\n const pair = [first.columns[0], second.columns[0]];\n const uniqueSets = uniqueColumnSets(table, meta.pks, context.uniques);\n if (!uniqueSets.some((set) => sameColumnSet(set, pair))) return null;\n\n const fkColumns = new Set(pair);\n const payload = meta.columns.filter((column) => isPayloadColumn(column, meta.pks, fkColumns));\n if (payload.length > 0) return null;\n\n return {\n table,\n role: \"junction\",\n reason: `only relates ${first.foreignTable} to ${second.foreignTable}: two foreign keys, unique together, no other data`,\n junction: {\n sourceTable: first.foreignTable,\n sourceColumn: first.columns[0],\n targetTable: second.foreignTable,\n targetColumn: second.columns[0]\n }\n };\n}\n\n/**\n * A small, referenced, self-contained code list.\n *\n * Requires: something references it, it references nothing, it carries few and\n * simple payload columns, and it is small — where \"small\" is a real count, not\n * an estimate. `reltuples` is -1 on any table that has never been analyzed,\n * which is every table in a freshly restored dump, so a count that is merely\n * absent must not read as \"small\".\n */\nfunction classifyLookup(table: string, context: AnalysisContext): TableClassification | null {\n if (!isLookupCandidate(table, context)) return null;\n\n const rowCount = context.rowCounts[table];\n if (typeof rowCount !== \"number\") return null;\n // An empty table is not a code list — there is nothing to look up, and a\n // schema-only restore would otherwise classify half the database this way.\n if (rowCount < 1 || rowCount > LOOKUP_MAX_ROWS) return null;\n\n const referencedBy = (context.inboundByTable.get(table) ?? []).length;\n return {\n table,\n role: \"lookup\",\n reason: `a ${rowCount}-row code list: referenced by ${referencedBy} table(s), references none, ${LOOKUP_MAX_PAYLOAD_COLUMNS} or fewer simple columns`\n };\n}\n\n/**\n * The structural half of the lookup test — everything except the row count.\n *\n * Separate because the count costs a query per table, and the caller only wants\n * to pay it for tables that could possibly qualify.\n */\nfunction isLookupCandidate(table: string, context: AnalysisContext): boolean {\n const meta = context.tables.get(table);\n if (!meta) return false;\n\n if ((context.constraintsByTable.get(table) ?? []).length > 0) return false;\n if ((context.inboundByTable.get(table) ?? []).length === 0) return false;\n\n const payload = meta.columns.filter((column) => isPayloadColumn(column, meta.pks, new Set()));\n if (payload.length > LOOKUP_MAX_PAYLOAD_COLUMNS) return false;\n\n // A code list holds labels and flags. Documents, arrays, files and dates are\n // content, and content means the rows are worth browsing in their own right.\n //\n // Strings are allowed whether or not they declare a length. Requiring\n // `varchar(n)` looked like a way to say \"label, not prose\", and it is —\n // except that a schema written any time recently uses `text` for everything,\n // which is the advice Postgres itself gives. Modern pagila is all `text`, so\n // that rule classified nothing at all on it.\n return payload.every((column) => {\n const type = mapPgType(column.data_type);\n return type === \"boolean\" || type === \"number\" || type === \"string\";\n });\n}\n\n/**\n * Names the tables whose classification depends on a row count.\n *\n * The caller counts these — and only these — before calling\n * {@link classifyTables}. On a schema of any size this is a handful of tables,\n * and the count itself is capped (see `countRowsUpTo`), so the whole extra cost\n * is bounded regardless of how much data the database holds.\n */\nexport function lookupCandidates(metadata: SchemaMetadata, tables: Map<string, TableMeta>): string[] {\n const context = buildContext(metadata, tables);\n return Array.from(tables.keys()).filter((table) => isLookupCandidate(table, context));\n}\n\n/**\n * Rows that belong to one parent row and are reached through it.\n *\n * Requires nothing to reference the table — a table others point at has an\n * identity of its own and belongs in the navigation — and an *unambiguous*\n * owner among its foreign keys. The evidence ladder runs strongest first; each\n * rung is only taken when exactly one key satisfies it, so a table with two\n * equally plausible parents stays an entity rather than being filed under a\n * coin-flip.\n */\nfunction classifyOwnedChild(table: string, context: AnalysisContext): TableClassification | null {\n const meta = context.tables.get(table);\n if (!meta) return null;\n\n const constraints = context.constraintsByTable.get(table) ?? [];\n if (constraints.length === 0) return null;\n if ((context.inboundByTable.get(table) ?? []).length > 0) return null;\n\n const owner = pickOwner(meta, constraints);\n if (!owner) return null;\n\n return {\n table,\n role: \"owned-child\",\n reason: `belongs to ${owner.constraint.foreignTable} (${describeEvidence(owner.evidence)}) and nothing else references it`,\n owner: {\n table: owner.constraint.foreignTable,\n column: owner.constraint.columns[0],\n evidence: owner.evidence\n }\n };\n}\n\nfunction describeEvidence(evidence: OwnershipEvidence): string {\n switch (evidence) {\n case \"cascade-delete\": return \"its only ON DELETE CASCADE foreign key\";\n case \"identifying-key\": return \"its only foreign key inside the primary key\";\n case \"sole-required-key\": return \"its only NOT NULL foreign key\";\n case \"leading-key-column\": return \"the leading column of a primary key made only of foreign keys\";\n }\n}\n\nfunction pickOwner(\n meta: TableMeta,\n constraints: ForeignKeyConstraint[]\n): { constraint: ForeignKeyConstraint; evidence: OwnershipEvidence } | null {\n const singleColumn = constraints.filter((c) => c.columns.length === 1);\n if (singleColumn.length === 0) return null;\n\n const nullability = new Map(meta.columns.map((c) => [c.column_name, c.is_nullable === \"NO\"]));\n\n const cascading = singleColumn.filter((c) => c.deleteRule === \"CASCADE\");\n if (cascading.length === 1) return { constraint: cascading[0], evidence: \"cascade-delete\" };\n\n const identifying = singleColumn.filter((c) => meta.pks.includes(c.columns[0]));\n if (identifying.length === 1) return { constraint: identifying[0], evidence: \"identifying-key\" };\n\n const required = singleColumn.filter((c) => nullability.get(c.columns[0]) === true);\n if (required.length === 1) return { constraint: required[0], evidence: \"sole-required-key\" };\n\n // Last resort, and the only rung that leans on an ordering convention rather\n // than a guarantee: in a table whose whole primary key is foreign keys, the\n // first key column is the parent the rows are filed under.\n // `northwind.order_details` is keyed `(order_id, product_id)` — the rows are\n // lines of an order, which is exactly what that ordering says.\n if (identifying.length === meta.pks.length && meta.pks.length > 1) {\n const leading = identifying.find((c) => c.columns[0] === meta.pks[0]);\n if (leading) return { constraint: leading, evidence: \"leading-key-column\" };\n }\n\n return null;\n}\n\n/**\n * Classifies every table in the schema.\n *\n * Order matters: junction is the most specific and most consequential (the\n * table disappears), so it is tested first; then lookup, which needs no\n * ownership reasoning; then ownership. Anything unmatched is an entity, which\n * is also what every rule falls back to when its evidence is ambiguous.\n */\nexport function classifyTables(\n metadata: SchemaMetadata,\n tables: Map<string, TableMeta>\n): Map<string, TableClassification> {\n const context = buildContext(metadata, tables);\n const result = new Map<string, TableClassification>();\n\n for (const table of tables.keys()) {\n const classification =\n classifyJunction(table, context) ??\n classifyLookup(table, context) ??\n classifyOwnedChild(table, context) ?? {\n table,\n role: \"entity\" as const,\n reason: \"no structural evidence that it is a join table, a code list, or owned by another table\"\n };\n result.set(table, classification);\n }\n\n return result;\n}\n\n// ── Presentation derived from structure ───────────────────────────────\n\n/**\n * The columns a property-level derivation needs, resolved once.\n */\nexport interface ColumnFacts {\n column: TableColumn;\n isPk: boolean;\n isFk: boolean;\n /** Covered by a single-column unique constraint or unique index. */\n isUniqueAlone: boolean;\n isAutoTimestamp: boolean;\n isGenerated: boolean;\n /** Allowed values, from a Postgres enum type or a readable CHECK. */\n enumValues?: string[];\n propType: string;\n}\n\nexport function buildColumnFacts(\n meta: TableMeta,\n metadata: SchemaMetadata,\n enumMap: Map<string, string[]>,\n checkFacts: CheckFactsByTable\n): Map<string, ColumnFacts> {\n const fkColumns = new Set(meta.fks.map((fk) => fk.column_name));\n const singleColumnUniques = new Set(\n metadata.uniques\n .filter((u) => u.table_name === meta.name && u.column_names.length === 1)\n .map((u) => u.column_names[0])\n );\n const tableChecks = checkFacts.get(meta.name);\n\n const facts = new Map<string, ColumnFacts>();\n for (const column of meta.columns) {\n const pgEnum = column.data_type === \"USER-DEFINED\" ? enumMap.get(column.udt_name) : undefined;\n const checkEnum = tableChecks?.get(column.column_name)?.enumValues;\n facts.set(column.column_name, {\n column,\n isPk: meta.pks.includes(column.column_name),\n isFk: fkColumns.has(column.column_name),\n isUniqueAlone: singleColumnUniques.has(column.column_name),\n isAutoTimestamp: isAutoTimestamp(column),\n isGenerated: isGeneratedColumn(column),\n enumValues: pgEnum ?? checkEnum,\n propType: pgEnum ? \"string\" : mapPgType(column.data_type)\n });\n }\n return facts;\n}\n\n/**\n * The column that identifies a row to a human.\n *\n * Structural, in three rungs, strongest first:\n *\n * 1. A single-column unique constraint on a required string. This is as close\n * as a schema comes to declaring \"this is what a row is called\": it is the\n * column a person looks a row up by, and the database guarantees it picks\n * out one row.\n * 2. The first required string that declares a length, when the table also has\n * strings that do not. Choosing `varchar(n)` for one column and `text` for\n * another is the author distinguishing a label from prose.\n * 3. The first required string in declaration order. Weak, but it is the same\n * rung the panel's own fallback stands on, and column order carries real\n * information — the identifying column of a table is written near the top of\n * it, in every schema, in every language.\n *\n * Deliberately not: a column called `name`, or `title`. That works on English\n * schemas written by someone who read the same tutorial. This picks\n * `film.title`, `actor.first_name` and `category.name` out of pagila without\n * knowing what any of those words mean.\n */\nexport function deriveTitleProperty(facts: Map<string, ColumnFacts>): string | undefined {\n const candidates = Array.from(facts.values()).filter((f) =>\n !f.isPk && !f.isFk && !f.isGenerated &&\n f.propType === \"string\" &&\n !f.enumValues &&\n f.column.is_nullable === \"NO\"\n );\n if (candidates.length === 0) return undefined;\n\n const unique = candidates.find((f) => f.isUniqueAlone);\n if (unique) return unique.column.column_name;\n\n const bounded = candidates.filter((f) => isBoundedString(f.column));\n if (bounded.length > 0 && bounded.length < candidates.length) {\n return bounded[0].column.column_name;\n }\n\n return candidates[0].column.column_name;\n}\n\n/**\n * The enum column a board should have as its columns.\n *\n * A board needs a small, closed, always-present set of states. `NOT NULL` is\n * required because a null has no column to sit in; the bounds keep out\n * two-state flags (a filter, not a board) and long code lists (a scrolling\n * table). The first qualifying column in declaration order wins, so the output\n * is stable across runs.\n */\nexport function deriveKanbanProperty(facts: Map<string, ColumnFacts>): string | undefined {\n for (const fact of facts.values()) {\n if (fact.isPk || fact.isFk || fact.isGenerated) continue;\n if (!fact.enumValues) continue;\n if (fact.column.is_nullable !== \"NO\") continue;\n if (fact.enumValues.length < KANBAN_MIN_VALUES || fact.enumValues.length > KANBAN_MAX_VALUES) continue;\n return fact.column.column_name;\n }\n return undefined;\n}\n\n/**\n * The column a list should be sorted by, newest first.\n *\n * Only when the table has exactly one database-maintained timestamp. With two —\n * a created and an updated stamp — the two orderings differ and the schema does\n * not say which the user means, so neither is chosen.\n */\nexport function deriveSort(facts: Map<string, ColumnFacts>): [string, \"desc\"] | undefined {\n const stamps = Array.from(facts.values()).filter((f) => f.isAutoTimestamp);\n if (stamps.length !== 1) return undefined;\n return [stamps[0].column.column_name, \"desc\"];\n}\n\n/**\n * The first `LIST_PROPERTIES_CAP` visible properties, or nothing.\n *\n * Returning nothing when the table is already narrow matters: `listProperties`\n * that restates every column is config the reader has to check against the\n * property list to discover it does nothing, and it silently stops new columns\n * from appearing in the list view when someone adds one later.\n *\n * `hidden` names the properties already marked `hideFromCollection` — spending\n * one of six columns on a value the list does not render is worse than not\n * capping at all.\n */\nexport function deriveListProperties(propertiesOrder: string[], hidden: ReadonlySet<string> = new Set()): string[] | undefined {\n const visible = propertiesOrder.filter((key) => !hidden.has(key));\n if (visible.length <= LIST_PROPERTIES_CAP) return undefined;\n return visible.slice(0, LIST_PROPERTIES_CAP);\n}\n","/**\n * Introspection logic — pure functions and the pipeline that transforms\n * raw PostgreSQL metadata into Rebase collection definition files.\n *\n * This module contains NO side-effects: no fs writes, no pg.Client creation,\n * no process.exit. It is imported by introspect-db.ts (the CLI entry-point)\n * and consumed directly by tests.\n */\nimport { firstFreeKey, toWireKey } from \"@rebasepro/utils\";\nimport { inferPropertyFromData } from \"./introspect-db-inference\";\nimport { humanize } from \"./introspect-db-naming\";\nimport { mapPgType } from \"./introspect-db-types\";\nimport type { CheckFactsByTable } from \"./introspect-db-constraints\";\nimport type { TableClassification } from \"./introspect-db-structure\";\nimport {\n buildColumnFacts,\n deriveKanbanProperty,\n deriveListProperties,\n deriveSort,\n deriveTitleProperty,\n isDerivedIndexColumn,\n isReadOnlyColumn\n} from \"./introspect-db-structure\";\n\n// ── Typed interfaces for SQL query results ────────────────────────────\n\nexport interface TableRow {\n table_name: string;\n /** True for the parent of a partitioned table (`relkind = 'p'`). */\n is_partitioned?: boolean;\n}\n\nexport interface TableColumn {\n table_name: string;\n column_name: string;\n data_type: string;\n udt_name: string;\n is_nullable: string;\n column_default: string | null;\n atttypmod: number | null;\n /** 1-based position in the table, as declared. */\n ordinal_position?: number;\n /** `\"ALWAYS\"` for a generated column, `\"NEVER\"` otherwise. */\n is_generated?: string;\n /** The `GENERATED ALWAYS AS (…)` expression, when `is_generated` says so. */\n generation_expression?: string | null;\n /** `\"YES\"` for an identity column. */\n is_identity?: string;\n /** `\"ALWAYS\"` or `\"BY DEFAULT\"` on an identity column. */\n identity_generation?: string | null;\n /** The declared `varchar(n)` / `char(n)` bound, if any. */\n character_maximum_length?: number | null;\n numeric_precision?: number | null;\n numeric_scale?: number | null;\n}\n\nexport interface EnumValue {\n enum_name: string;\n enum_value: string;\n sort_order: number;\n}\n\nexport interface PrimaryKeyRow {\n table_name: string;\n column_name: string;\n}\n\nexport interface ForeignKeyRow {\n table_name: string;\n column_name: string;\n foreign_table_name: string;\n foreign_column_name: string;\n /** Name of the FK constraint — the only way to tell composite keys apart. */\n constraint_name?: string;\n /** 1-based position of this column within its constraint. */\n ordinal?: number;\n /** `\"CASCADE\"`, `\"RESTRICT\"`, `\"SET NULL\"`, `\"SET DEFAULT\"`, `\"NO ACTION\"`. */\n delete_rule?: string;\n}\n\n/** A unique constraint or unique index, as an ordered column list. */\nexport interface UniqueConstraintRow {\n table_name: string;\n constraint_name: string;\n column_names: string[];\n}\n\n/** A CHECK constraint, as `pg_get_constraintdef` renders it. */\nexport interface CheckConstraintRow {\n table_name: string;\n constraint_name: string;\n definition: string;\n}\n\n/** A `COMMENT ON TABLE` (null `column_name`) or `COMMENT ON COLUMN`. */\nexport interface CommentRow {\n table_name: string;\n column_name: string | null;\n comment: string;\n}\n\n/**\n * Everything one introspection run reads from the database.\n *\n * Passed around as one value so a new signal means a new field here rather than\n * a new parameter on every function between the query and the generator — the\n * shape `generateCollectionFile` had grown to seven positional arguments by.\n */\nexport interface SchemaMetadata {\n schema: string;\n tables: TableRow[];\n columns: TableColumn[];\n enumValues: EnumValue[];\n pks: PrimaryKeyRow[];\n fks: ForeignKeyRow[];\n uniques: UniqueConstraintRow[];\n checks: CheckConstraintRow[];\n comments: CommentRow[];\n /**\n * Row counts for the tables that needed one, capped — see `countRowsUpTo`.\n * Absent for every table introspection never had a reason to count.\n */\n rowCounts: Record<string, number>;\n}\n\nexport interface TableMeta {\n name: string;\n columns: TableColumn[];\n pks: string[];\n fks: ForeignKeyRow[];\n}\n\n// ── Irregular plurals that naive rules can't handle ───────────────────\n\nconst IRREGULAR_SINGULARS: Record<string, string> = {\n people: \"person\",\n children: \"child\",\n men: \"man\",\n women: \"woman\",\n mice: \"mouse\",\n geese: \"goose\",\n teeth: \"tooth\",\n feet: \"foot\",\n data: \"datum\",\n media: \"medium\",\n criteria: \"criterion\",\n phenomena: \"phenomenon\"\n};\n\n/**\n * Plurals in \"-ves\" whose singular really ends in f/fe, and which of the two.\n *\n * A blanket \"-ves\" -> \"-f\" rule gets `knives` -> `knif`, and mangles every\n * ordinary \"-ive\" noun that happens to be plural along with it: `archives` ->\n * `archif`, `objectives` -> `objectif`. The set of English words that genuinely\n * swap f/fe for ves is small and closed, so it is listed rather than guessed —\n * anything else ending in \"ves\" drops the trailing 's' like any other plural.\n *\n * Matched on the whole word, not on the suffix: `olives` ends in `lives`.\n */\nconst VES_SINGULAR_ENDINGS: Record<string, \"f\" | \"fe\"> = {\n calves: \"f\", dwarves: \"f\", elves: \"f\", halves: \"f\", hooves: \"f\",\n leaves: \"f\", loaves: \"f\", scarves: \"f\", selves: \"f\", sheaves: \"f\",\n shelves: \"f\", thieves: \"f\", wharves: \"f\", wolves: \"f\",\n knives: \"fe\", lives: \"fe\", wives: \"fe\"\n};\n\n/** Words ending in 's' that are already singular. */\nconst UNCOUNTABLE = new Set([\n \"status\", \"campus\", \"virus\", \"bus\", \"plus\", \"census\",\n \"diagnosis\", \"analysis\", \"basis\", \"crisis\", \"thesis\",\n \"synopsis\", \"parenthesis\", \"hypothesis\", \"emphasis\",\n \"news\", \"series\", \"species\", \"means\", \"athletics\",\n \"economics\", \"electronics\", \"mathematics\", \"physics\",\n \"politics\", \"statistics\"\n]);\n\nexport function singularize(word: string): string {\n const lower = word.toLowerCase();\n\n // Check irregular forms\n if (IRREGULAR_SINGULARS[lower]) {\n // Preserve the original casing of the first character\n const singular = IRREGULAR_SINGULARS[lower];\n return word[0] === word[0].toUpperCase()\n ? singular.charAt(0).toUpperCase() + singular.slice(1)\n : singular;\n }\n\n // Check uncountable\n if (UNCOUNTABLE.has(lower)) return word;\n\n // Latin/Greek -es endings (diagnosis -> diagnosis is uncountable, but \"addresses\" -> \"address\")\n if (lower.endsWith(\"ices\") && lower.length > 5) {\n // e.g. \"indices\" -> \"index\", \"vertices\" -> \"vertex\"\n return word.slice(0, -4) + \"ex\";\n }\n if (lower.endsWith(\"ies\") && lower.length > 3) {\n return word.slice(0, -3) + \"y\";\n }\n if (VES_SINGULAR_ENDINGS[lower]) {\n // e.g. \"wolves\" -> \"wolf\", \"knives\" -> \"knife\"\n return word.slice(0, -3) + VES_SINGULAR_ENDINGS[lower];\n }\n if (lower.endsWith(\"ches\") || lower.endsWith(\"shes\") || lower.endsWith(\"sses\") || lower.endsWith(\"xes\") || lower.endsWith(\"zes\")) {\n return word.slice(0, -2);\n }\n if (lower.endsWith(\"ses\") && !lower.endsWith(\"sses\")) {\n // e.g. \"responses\" -> \"response\", \"databases\" -> \"database\"\n return word.slice(0, -1);\n }\n if (lower.endsWith(\"s\") && !lower.endsWith(\"ss\") && !lower.endsWith(\"us\") && !lower.endsWith(\"is\")) {\n return word.slice(0, -1);\n }\n\n return word;\n}\n\n/**\n * Convert a snake_case table name to a camelCase + \"Collection\" variable name.\n * e.g. \"company_token\" -> \"companyTokenCollection\"\n */\nexport function toCollectionVarName(tableName: string): string {\n const camel = tableName.replace(/_([a-z])/g, (_g, letter: string) => letter.toUpperCase()) + \"Collection\";\n // Only reshapes names that are not identifiers already, so every table that\n // generated a working file keeps the exact variable name it had. A table\n // called `2024 archive` used to emit `const 2024 archiveCollection`, which\n // is three syntax errors rather than a declaration.\n if (JS_IDENTIFIER.test(camel)) return camel;\n const sanitized = camel.replace(/[^A-Za-z0-9_$]/g, \"_\");\n return /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized;\n}\n\nexport function getIconForTable(tableName: string): string {\n const table = tableName.toLowerCase();\n if (table.includes(\"user\") || table.includes(\"account\") || table.includes(\"member\") || table.includes(\"customer\") || table.includes(\"client\") || table.includes(\"patient\")) return \"Users\";\n if (table.includes(\"post\") || table.includes(\"article\") || table.includes(\"blog\") || table.includes(\"page\")) return \"FileText\";\n if (table.includes(\"product\") || table.includes(\"item\")) return \"Package\";\n if (table.includes(\"order\") || table.includes(\"cart\") || table.includes(\"purchase\") || table.includes(\"invoice\")) return \"ShoppingCart\";\n if (table.includes(\"setting\") || table.includes(\"config\")) return \"Settings\";\n if (table.includes(\"tag\") || table.includes(\"categor\")) return \"Tag\";\n if (table.includes(\"image\") || table.includes(\"photo\") || table.includes(\"media\") || table.includes(\"asset\")) return \"Image\";\n if (table.includes(\"notification\") || table.includes(\"message\") || table.includes(\"email\")) return \"Mail\";\n if (table.includes(\"log\") || table.includes(\"audit\") || table.includes(\"event\")) return \"Activity\";\n if (table.includes(\"subscription\") || table.includes(\"plan\") || table.includes(\"billing\")) return \"CreditCard\";\n if (table.includes(\"comment\") || table.includes(\"review\") || table.includes(\"feedback\")) return \"MessageCircle\";\n return \"Database\";\n}\n\nexport { mapPgType };\n\n// ── Build the enum map from query results ─────────────────────────────\n\nexport function buildEnumMap(enumValues: EnumValue[]): Map<string, string[]> {\n const enumMap = new Map<string, string[]>();\n for (const ev of enumValues) {\n const existing = enumMap.get(ev.enum_name);\n if (existing) {\n existing.push(ev.enum_value);\n } else {\n enumMap.set(ev.enum_name, [ev.enum_value]);\n }\n }\n return enumMap;\n}\n\n// ── Build the tables map from raw query results ───────────────────────\n\nexport function buildTablesMap(\n tables: TableRow[],\n columns: TableColumn[],\n pks: PrimaryKeyRow[],\n fks: ForeignKeyRow[]\n): Map<string, TableMeta> {\n const tablesMap = new Map<string, TableMeta>();\n for (const t of tables) {\n tablesMap.set(t.table_name, {\n name: t.table_name,\n columns: columns.filter((c) => c.table_name === t.table_name),\n pks: pks.filter((pk) => pk.table_name === t.table_name).map((pk) => pk.column_name),\n fks: fks.filter((fk) => fk.table_name === t.table_name)\n });\n }\n return tablesMap;\n}\n\n// ── Identify join tables ──────────────────────────────────────────────\n\n/**\n * Join tables, identified by column name.\n *\n * Superseded for the CLI by `classifyTables` in `./introspect-db-structure`,\n * which asks the database instead: two single-column keys, unique together, no\n * payload column, nothing referencing the table. This rule folds away\n * `northwind.order_details` — which has the key shape and carries unit price,\n * quantity and discount — because it recognises `id`, `created_at` and\n * `updated_at` by name and calls everything else a foreign key.\n *\n * Still used by `./introspect-runtime`, which builds collections in memory from\n * a narrower set of catalog queries and has no unique-constraint or row-count\n * data to reason with.\n */\nexport function identifyJoinTables(tablesMap: Map<string, TableMeta>): Set<string> {\n const joinTables = new Set<string>();\n for (const [tableName, meta] of tablesMap.entries()) {\n if (meta.fks.length === 2) {\n const isLikelyJoinTable = meta.columns.every((c) =>\n meta.fks.some((fk) => fk.column_name === c.column_name) ||\n c.column_name === \"id\" ||\n c.column_name === \"created_at\" ||\n c.column_name === \"updated_at\"\n );\n\n if (isLikelyJoinTable) {\n joinTables.add(tableName);\n }\n }\n }\n return joinTables;\n}\n\n// ── Property ordering heuristics ──────────────────────────────────────\n\n/**\n * Property metadata used to compute display priority.\n * Keeps computePropertyPriority free of any TableMeta coupling.\n */\nexport interface PropertyOrderingContext {\n /** The resolved Rebase property type (e.g. \"string\", \"number\", \"date\", \"relation\"). */\n propType: string;\n /** Whether this column is a primary key. */\n isPk: boolean;\n /** Whether this column is an enum (USER-DEFINED with matching values). */\n isEnum: boolean;\n /** Whether this is a storage/file-upload field (detected from column name). */\n isStorage: boolean;\n /** The PostgreSQL data_type (e.g. \"text\", \"character varying\", \"jsonb\"). */\n pgDataType: string;\n /** The original column index in PostgreSQL (for stable tiebreaking). */\n originalIndex: number;\n}\n\n// — Tier 0: Identity (0–9) ————————————————————————————————————————————\nconst IDENTITY_EXACT: Record<string, number> = {\n id: 0,\n uuid: 1,\n _id: 2\n};\n\n// — Tier 1: Title / Name — the \"display column\" (10–19) ———————————————\nconst TITLE_EXACT: Record<string, number> = {\n name: 10,\n title: 11,\n label: 12,\n display_name: 13,\n displayname: 13,\n headline: 14,\n subject: 15,\n heading: 16\n};\n\n// — Tier 2: Human identity fields (20–29) —————————————————————————————\nconst HUMAN_IDENTITY_EXACT: Record<string, number> = {\n first_name: 20,\n firstname: 20,\n last_name: 21,\n lastname: 21,\n full_name: 22,\n fullname: 22,\n given_name: 22,\n family_name: 23,\n middle_name: 24,\n username: 25,\n user_name: 25,\n email: 26,\n email_address: 26,\n phone: 27,\n phone_number: 27,\n mobile: 27\n};\n\n// — Tier 3: Core descriptors (30–39) ——————————————————————————————————\nconst DESCRIPTOR_EXACT: Record<string, number> = {\n slug: 30,\n code: 31,\n sku: 32,\n reference: 33,\n ref: 33,\n type: 34,\n kind: 34,\n status: 35,\n state: 35,\n role: 36,\n category: 37,\n group: 38,\n priority: 39,\n order: 39,\n sort_order: 39,\n position: 39\n};\n\n// — Tier 12: System timestamps (120–129) ——————————————————————————————\nconst SYSTEM_TIMESTAMP_EXACT: Record<string, number> = {\n created_at: 120,\n createdat: 120,\n creation_date: 120,\n inserted_at: 121,\n updated_at: 122,\n updatedat: 122,\n modified_at: 122,\n last_modified: 122,\n deleted_at: 123,\n deletedat: 123,\n archived_at: 124\n};\n\n// — Pattern-based rules for partial matches ———————————————————————————\nconst TITLE_PATTERNS = [\"name\", \"title\", \"label\"];\nconst LONG_TEXT_NAMES = new Set([\"description\", \"summary\", \"excerpt\", \"abstract\", \"overview\", \"bio\", \"biography\", \"about\"]);\nconst RICH_CONTENT_NAMES = new Set([\"content\", \"body\", \"html\", \"markup\", \"text\", \"article_body\", \"post_body\"]);\nconst MEDIA_PATTERNS = [\"image\", \"avatar\", \"photo\", \"logo\", \"cover\", \"thumbnail\", \"banner\", \"icon\", \"picture\", \"poster\"];\nconst JSON_MAP_NAMES = new Set([\"metadata\", \"meta\", \"config\", \"configuration\", \"settings\", \"options\", \"preferences\", \"data\", \"payload\", \"attributes\", \"extra\", \"additional_info\"]);\n\n/**\n * Compute a numeric priority score for a property.\n * Lower scores appear first in the generated `propertiesOrder` array.\n *\n * The system uses 14 tiers (0–139), with the original column index\n * added as a fractional tiebreaker (originalIndex / 10000) to\n * guarantee stable ordering within the same tier.\n *\n * Pure function — no side effects.\n */\nexport function computePropertyPriority(\n columnName: string,\n ctx: PropertyOrderingContext\n): number {\n // Normalize camelCase/PascalCase to snake_case, then lowercase\n const col = columnName.replace(/([a-z0-9])([A-Z])/g, \"$1_$2\").toLowerCase();\n const tiebreaker = ctx.originalIndex / 10000;\n\n // ── Tier 0: Primary key identity fields\n if (ctx.isPk) {\n const exactScore = IDENTITY_EXACT[col];\n return (exactScore ?? 5) + tiebreaker;\n }\n\n // ── Tier 12: System timestamps (check early to prevent false matches)\n const systemTs = SYSTEM_TIMESTAMP_EXACT[col];\n if (systemTs !== undefined) {\n return systemTs + tiebreaker;\n }\n\n // ── Tier 1: Title / Name exact matches\n const titleExact = TITLE_EXACT[col];\n if (titleExact !== undefined) {\n return titleExact + tiebreaker;\n }\n\n // ── Tier 2: Human identity exact matches\n const humanExact = HUMAN_IDENTITY_EXACT[col];\n if (humanExact !== undefined) {\n return humanExact + tiebreaker;\n }\n\n // ── Tier 3: Core descriptor exact matches\n const descriptorExact = DESCRIPTOR_EXACT[col];\n if (descriptorExact !== undefined) {\n return descriptorExact + tiebreaker;\n }\n\n // ── Tier 1b: Title-like partial matches (e.g. \"product_name\", \"page_title\")\n // Score 17–19 so they rank after exact matches but still in tier 1.\n for (const pattern of TITLE_PATTERNS) {\n if (col.includes(pattern) && col !== pattern) {\n return 17 + tiebreaker;\n }\n }\n\n // ── Tier 9: Media / file upload fields (check before general strings)\n if (ctx.isStorage) {\n return 90 + tiebreaker;\n }\n for (const pattern of MEDIA_PATTERNS) {\n if (col.includes(pattern)) {\n return 91 + tiebreaker;\n }\n }\n if (col.endsWith(\"_url\") || col.endsWith(\"_uri\") || col.endsWith(\"_link\")) {\n return 92 + tiebreaker;\n }\n\n // ── Tier 7: Long text fields\n if (LONG_TEXT_NAMES.has(col)) {\n return 70 + tiebreaker;\n }\n\n // ── Tier 8: Rich content fields\n if (RICH_CONTENT_NAMES.has(col)) {\n return 80 + tiebreaker;\n }\n\n // ── Tier 10: JSON / Map types\n if (ctx.propType === \"map\") {\n return JSON_MAP_NAMES.has(col) ? 100 + tiebreaker : 105 + tiebreaker;\n }\n\n // ── Tier 11: Array types\n if (ctx.propType === \"array\") {\n return 110 + tiebreaker;\n }\n\n // ── Tier 6: Owning relations\n if (ctx.propType === \"relation\") {\n return 60 + tiebreaker;\n }\n\n // ── Tier 4: Short text, enums, booleans — \"quick glance\" fields\n if (ctx.isEnum) {\n return 40 + tiebreaker;\n }\n if (ctx.propType === \"boolean\") {\n return 45 + tiebreaker;\n }\n if (ctx.propType === \"string\" && ctx.pgDataType !== \"text\") {\n // Short string (varchar, char, uuid that's not a PK)\n return 42 + tiebreaker;\n }\n\n // ── Tier 5: Numbers & user-facing dates\n if (ctx.propType === \"number\") {\n return 50 + tiebreaker;\n }\n if (ctx.propType === \"date\") {\n // A date that isn't a system timestamp (already handled above)\n return 55 + tiebreaker;\n }\n\n // ── Tier 7b: text data_type that didn't match long-text names\n if (ctx.propType === \"string\" && ctx.pgDataType === \"text\") {\n return 75 + tiebreaker;\n }\n\n // ── Tier 13: Fallback / unknown\n return 130 + tiebreaker;\n}\n\n/**\n * Sort a `propertiesOrder` array using the priority heuristic.\n * Returns a new sorted array; does not mutate the input.\n *\n * @param entries - Array of { key, columnName, propType, ... } objects\n * carrying the information needed to compute priority.\n */\nexport interface PropertyOrderEntry {\n /** The property key in the generated collection (may differ from columnName for relations). */\n key: string;\n /** The ordering context for this property. */\n ctx: PropertyOrderingContext;\n}\n\nexport function sortPropertiesOrder(entries: PropertyOrderEntry[]): string[] {\n return [...entries]\n .sort((a, b) => computePropertyPriority(a.key, a.ctx) - computePropertyPriority(b.key, b.ctx))\n .map((e) => e.key);\n}\n\n// ── Generate collection file content ──────────────────────────────────\n\nexport interface GeneratedFile {\n tableName: string;\n fileName: string;\n content: string;\n}\n\n/**\n * The structural analysis a run can hand the generator.\n *\n * Optional in full, and the generator degrades to exactly its previous output\n * without it. That is not politeness towards old callers: three existing test\n * suites and the `rebase init` scaffold path build a `TableMeta` by hand and\n * have no database to read constraints or row counts from, and they must keep\n * producing a valid collection.\n */\n/**\n * Which `defineCollection` — if any — the project being generated into can import.\n *\n * A bare `const x: PostgresCollectionConfig = { … }` annotation widens `properties`\n * to `Record<string, …>`, and every key-shaped field in the admin block —\n * `titleProperty`, `sort`, `propertiesOrder`, `listProperties`, `fixedFilter` — is\n * derived from those keys. Annotated, they accept any string: introspection was\n * emitting a `propertiesOrder` array that nothing checked, so renaming a column and\n * re-introspecting left a stale key that compiled silently. `defineCollection` is\n * the identity function whose `const P` type parameter keeps the keys literal, which\n * is what turns that checking on.\n *\n * There are two of them and they are not interchangeable:\n *\n * - `admin-types` — `@rebasepro/cms-types`. Its index side-effect-imports\n * `augment.ts`, so importing it is also what *declares* the `admin` block. Only a\n * project that depends on the package can resolve it.\n * - `common` — `@rebasepro/common`. Same key inference, no admin surface, no React\n * anywhere in its graph (`scripts/headless-guard` lists it as core). This is the\n * headless flavour.\n * - `annotation` — neither package is declared, so neither import would resolve and\n * the old annotation is the only honest thing to emit. Projects scaffolded before\n * `@rebasepro/common` joined the headless config package land here.\n *\n * The last two emit **no admin block, on the collection or on any property**. That is\n * not a downgrade: `@rebasepro/types` declares no `admin` field at all, so the block\n * introspection used to emit was a type error in every headless project it was\n * written into. See `packages/cms-types/src/augment.ts`.\n */\nexport type CollectionBuilder = \"admin-types\" | \"common\" | \"annotation\";\n\n/**\n * The package specifiers the generated files name, spelled once.\n *\n * Written as constants rather than inline in the import templates below because\n * `scripts/headless-guard/check-types.mjs` scans core sources for `from\n * \"@rebasepro/cms-types\"` and cannot tell a real import from one this module\n * *writes*. It is right to be that blunt — the guard's whole value is that it\n * cannot be reasoned around — so the string simply never appears in that shape\n * here. Inlining them back into the templates re-breaks `check:types-headless`.\n */\nexport const ADMIN_TYPES_PACKAGE = \"@rebasepro/cms-types\";\nexport const COMMON_PACKAGE = \"@rebasepro/common\";\nexport const TYPES_PACKAGE = \"@rebasepro/types\";\n\nexport interface GenerationContext {\n metadata?: SchemaMetadata;\n classifications?: Map<string, TableClassification>;\n checkFacts?: CheckFactsByTable;\n /**\n * Defaults to `admin-types`, which is what the generator has always emitted.\n * The CLI never relies on the default — `introspect-db.ts` detects the flavour\n * from the target project and passes it. See `detectCollectionBuilder`.\n */\n builder?: CollectionBuilder;\n}\n\n/** Adds entries to a property's `validation` block, creating it if absent. */\nfunction withValidation(extra: string, entries: string[]): string {\n if (entries.length === 0) return extra;\n const block = entries.map((e) => ` ${e}`).join(\",\\n\");\n if (extra.includes(\"validation: {\")) {\n return extra.replace(\"validation: {\", `validation: {\\n${block},`);\n }\n return `${extra}\\n validation: {\\n${block}\\n },`;\n}\n\n/**\n * Whether a property's generated text already sets `key:` as an object key.\n *\n * A bare `extra.includes(\"min:\")` looks like it answers this and does not:\n * `admin:` ends in `min:`, so every property with an admin block claimed to\n * have a minimum already and silently lost the one the database declared. The\n * leading-delimiter requirement is the whole point — a key is preceded by a\n * newline, a brace or a comma, never by another identifier character.\n */\nfunction hasGeneratedKey(extra: string, key: string): boolean {\n return new RegExp(`(^|[\\\\s{,])${key}\\\\s*:`).test(extra);\n}\n\n/**\n * Adds entries to a property's `admin` block, creating it if absent.\n *\n * `emitAdmin` is false for the headless flavours, where `BaseProperty` has no\n * `admin` field to put them in — see {@link CollectionBuilder}. The options are\n * dropped rather than relocated: every one of them (`readOnly`, `multiline`,\n * `hideFromCollection`, `urlPreview`) describes a form widget, and there is no\n * form.\n */\nfunction withAdminOptions(extra: string, entries: string[], emitAdmin = true): string {\n if (!emitAdmin) return extra;\n if (entries.length === 0) return extra;\n const block = entries.map((e) => ` ${e}`).join(\",\\n\");\n if (extra.includes(\"admin: {\")) {\n return extra.replace(\"admin: {\", `admin: {\\n${block},`);\n }\n return `${extra}\\n admin: {\\n${block}\\n },`;\n}\n\n/** A TypeScript string literal, escaped. */\nfunction quote(value: string): string {\n return JSON.stringify(value);\n}\n\nconst JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * An object key for the generated file: verbatim when the name is a JavaScript\n * identifier, quoted otherwise.\n *\n * Postgres constrains an identifier only by quoting, so `order`, `full name`\n * and `2fa_enabled` are all ordinary column names — and all three produced a\n * file that did not parse when written as a bare key.\n *\n * Still needed now that keys are camel-cased rather than copied from the\n * column: `toWireKey` splits on separators and joins, which fixes `full name`\n * but cannot fix a name that is not an identifier for some other reason —\n * `2fa_enabled` becomes `2faEnabled`, still leading with a digit, and `order`\n * was never a separator problem at all.\n */\nfunction propKey(name: string): string {\n return JS_IDENTIFIER.test(name) ? name : quote(name);\n}\n\n/**\n * Text safe to put after `//`.\n *\n * A table comment or a classification reason is free text out of the database.\n * A newline in one ended the comment and let the rest of the value continue as\n * code.\n */\nfunction commentText(value: string): string {\n return value.replace(/\\s*[\\r\\n]+\\s*/g, \" \");\n}\n\n/** A property-key array, one key per line, indented for the admin block. */\nfunction formatKeyList(keys: string[]): string {\n if (keys.length === 0) return \"[]\";\n return `[\\n${keys.map((k) => ` ${quote(k)}`).join(\",\\n\")}\\n ]`;\n}\n\n/**\n * Generate the full TypeScript file content for a single collection.\n * Pure function — no I/O.\n */\nexport function generateCollectionFile(\n tableName: string,\n meta: TableMeta,\n allFks: ForeignKeyRow[],\n joinTables: Set<string>,\n tablesMap: Map<string, TableMeta>,\n enumMap: Map<string, string[]>,\n sampleData?: Record<string, unknown>[],\n context: GenerationContext = {}\n): string {\n const collectionName = humanize(tableName);\n const singular = singularize(collectionName);\n const icon = getIconForTable(tableName);\n\n const classification = context.classifications?.get(tableName);\n const checkFacts: CheckFactsByTable = context.checkFacts ?? new Map();\n const tableChecks = checkFacts.get(tableName);\n const columnComments = new Map<string, string>();\n let tableComment: string | undefined;\n for (const comment of context.metadata?.comments ?? []) {\n if (comment.table_name !== tableName) continue;\n if (comment.column_name === null) tableComment = comment.comment;\n else columnComments.set(comment.column_name, comment.comment);\n }\n const singleColumnUniques = new Set(\n (context.metadata?.uniques ?? [])\n .filter((u) => u.table_name === tableName && u.column_names.length === 1)\n .map((u) => u.column_names[0])\n );\n\n const builder: CollectionBuilder = context.builder ?? \"admin-types\";\n /** Whether the target project has an `admin` field to write into at all. */\n const emitAdmin = builder === \"admin-types\";\n\n const BUILDER_IMPORT: Record<CollectionBuilder, string> = {\n \"admin-types\": `import { defineCollection } from ${quote(ADMIN_TYPES_PACKAGE)};`,\n common: `import { defineCollection } from ${quote(COMMON_PACKAGE)};`,\n annotation: `import { PostgresCollectionConfig } from ${quote(TYPES_PACKAGE)};`\n };\n const imports = new Set<string>([BUILDER_IMPORT[builder]]);\n\n /**\n * Imports the collection a relation points at — unless it is this one.\n *\n * A self-referencing key (`employees.reports_to -> employees`, which both\n * northwind and chinook have) otherwise made the file import its own default\n * export under the name it declares three lines later: `TS2440: Import\n * declaration conflicts with local declaration`. The relation target is a\n * thunk, so referring to the local const directly is fine — it is only\n * dereferenced after the module has finished evaluating.\n */\n const importCollection = (otherTable: string): string => {\n const varName = toCollectionVarName(otherTable);\n if (otherTable !== tableName) imports.add(`import ${varName} from ${quote(`./${otherTable}`)};`);\n return varName;\n };\n\n /**\n * A relation's `target` thunk, with its return type spelled out.\n *\n * The annotation is what makes `defineCollection` survive a relational schema.\n * Without an explicit type on the const, the collection's type is *inferred*,\n * and a relation cycle — `posts` belongs to `authors`, `authors` has many\n * `posts`; or `employees.reports_to -> employees`, which northwind, chinook and\n * musicbrainz all have — makes that inference circular: `TS7022: implicitly has\n * type 'any' because it is referenced directly or indirectly in its own\n * initializer`, plus `TS7023` on the thunk and `TS2303` on the import alias.\n * Naming the return type lets the checker type the thunk without resolving the\n * collection it points at, which breaks the cycle. Nothing else is given up:\n * the inference that matters runs over `properties`, not over `relations`.\n *\n * The annotated flavour has no cycle to break — its const is already typed — so\n * it keeps the plainer thunk it has always emitted.\n */\n const relationTarget = (targetVarName: string): string => {\n if (builder === \"annotation\") return `() => ${targetVarName}`;\n imports.add(`import type { AnyCollectionConfig } from ${quote(TYPES_PACKAGE)};`);\n return `(): AnyCollectionConfig => ${targetVarName}`;\n };\n\n let propsOutput = \"\";\n let relationsOutput = \"\";\n const orderEntries: PropertyOrderEntry[] = [];\n const propertyBlocks = new Map<string, string>();\n /**\n * Column → the property key it was generated under.\n *\n * Needed because the structural helpers below (`deriveTitleProperty`,\n * `deriveKanbanProperty`, `deriveSort`) answer in *columns* — they read\n * `pg_attribute` — while `display.title`, `kanban.columnProperty` and\n * `sort` name **properties**. The two used to be the same string, so\n * nothing carried the translation; now `full_name` is generated as\n * `fullName` and a title pointing at `full_name` points at nothing.\n */\n const keyByColumn = new Map<string, string>();\n /** Properties the list view will not render, so `listProperties` skips them. */\n const hiddenFromCollection = new Set<string>();\n let columnIndex = 0;\n\n // Detect composite primary keys\n const isCompositePk = meta.pks.length > 1;\n\n // Map columns\n for (const col of meta.columns) {\n // Skip foreign keys since we handle them as relations\n // Exception: Do not skip if it's part of the primary key!\n if (meta.fks.some((fk) => fk.column_name === col.column_name) && !meta.pks.includes(col.column_name)) continue;\n\n const currentIndex = columnIndex++;\n\n // The key this column is generated under — its *wire* name, which is\n // not its column name. `columnName` below carries the column, so the\n // two never have to agree and the API stops carrying `user_id` next to\n // `displayName`.\n //\n // Camel-casing makes collisions possible where none existed: `user_id`\n // and `userId` are two columns and one key, which is a duplicate key in\n // an object literal — a TypeScript error that stops the whole generated\n // collection compiling. Resolved the same way the foreign-key loop\n // below resolves its own: first free candidate, then a numbered tail,\n // so no column is ever dropped. The raw column name is the second\n // candidate, so the loser of a collision still gets a name that means\n // something. Deterministic for a given database: the columns arrive in\n // ordinal order, so the same schema always yields the same keys.\n const propertyKey = firstFreeKey([toWireKey(col.column_name), col.column_name], propertyBlocks);\n keyByColumn.set(col.column_name, propertyKey);\n\n // Check if this column uses a PostgreSQL enum type\n const colEnumValues = enumMap.get(col.udt_name);\n const isEnumColumn = col.data_type === \"USER-DEFINED\" && colEnumValues !== undefined;\n const isVectorColumn = col.udt_name === \"vector\";\n\n const propType = isEnumColumn ? \"string\" : (isVectorColumn ? \"vector\" : mapPgType(col.data_type));\n let extra = \"\";\n\n const colNameLower = col.column_name.toLowerCase();\n\n // ── Data Inference Engine ────────────────────────────────────────────\n let finalPropType = propType;\n let inferenceExtra = \"\";\n\n if (!isEnumColumn && sampleData && sampleData.length > 0) {\n const values = sampleData.map(r => r[col.column_name]);\n const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);\n if (inferred.propType) finalPropType = inferred.propType;\n if (inferred.extra) inferenceExtra = inferred.extra;\n }\n\n const columnChecks = tableChecks?.get(col.column_name);\n\n // Enum values — generate real enum from the PG enum\n if (isEnumColumn && colEnumValues) {\n const enumEntries = colEnumValues\n .map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`)\n .join(\", \");\n extra += `\\n enum: [${enumEntries}],`;\n } else if (columnChecks?.enumValues && !inferenceExtra.includes(\"enum:\") && propType === \"string\") {\n // `CHECK (col IN (…))` is the other way a schema declares a closed\n // set. It is the same statement as a Postgres enum type, made by an\n // author who did not want a type — and until now the form offered a\n // free-text box for it and let the database reject the write.\n const enumEntries = columnChecks.enumValues\n .map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`)\n .join(\", \");\n extra += `\\n enum: [${enumEntries}],`;\n }\n\n // Date auto-value heuristics\n if (finalPropType === \"date\") {\n if (colNameLower === \"created_at\" || colNameLower === \"createdat\") {\n extra += \"\\n autoValue: \\\"on_create\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\", \"hideFromCollection: true\"], emitAdmin);\n hiddenFromCollection.add(propertyKey);\n } else if (colNameLower === \"updated_at\" || colNameLower === \"updatedat\") {\n extra += \"\\n autoValue: \\\"on_update\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\", \"hideFromCollection: true\"], emitAdmin);\n hiddenFromCollection.add(propertyKey);\n } else if (col.column_default && (col.column_default.includes(\"now()\") || col.column_default.includes(\"CURRENT_TIMESTAMP\"))) {\n extra += \"\\n autoValue: \\\"on_create\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\"], emitAdmin);\n }\n }\n\n // Array/Map heuristics (Fallback if not inferred)\n if (finalPropType === \"array\" && !inferenceExtra.includes(\"of: {\")) {\n let innerType = \"string\";\n let colType = \"\";\n if (col.udt_name.startsWith(\"_\")) {\n const baseType = col.udt_name.substring(1);\n innerType = mapPgType(baseType);\n if (innerType === \"string\") colType = \"text[]\";\n else if (innerType === \"number\") colType = col.udt_name === \"_numeric\" ? \"numeric[]\" : \"integer[]\";\n else if (innerType === \"boolean\") colType = \"boolean[]\";\n }\n if (colType) {\n extra += `\\n columnType: ${quote(colType)},`;\n }\n extra += `\\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(innerType)} },`;\n } else if (finalPropType === \"map\" && !inferenceExtra.includes(\"keyValue: true\") && !inferenceExtra.includes(\"properties: {\")) {\n extra += \"\\n keyValue: true,\";\n }\n\n // String sub-type heuristics (Fallback if not handled by inference or enum)\n if (finalPropType === \"string\" && !isEnumColumn && !inferenceExtra) {\n const isUrl = colNameLower.endsWith(\"_url\") || colNameLower.endsWith(\"_uri\") || colNameLower.endsWith(\"_link\");\n const isMedia = colNameLower.includes(\"image\") || colNameLower.includes(\"avatar\") || colNameLower.includes(\"photo\") || colNameLower.includes(\"logo\") || colNameLower.includes(\"cover\");\n\n if (isMedia) {\n extra += `\\n storage: {\\n storagePath: ${quote(`${tableName}/${col.column_name}`)}\\n },`;\n } else if (isUrl) {\n extra += \"\\n url: true,\";\n } else if (colNameLower === \"description\" || colNameLower === \"summary\" || colNameLower === \"excerpt\") {\n extra = withAdminOptions(extra, [\"multiline: true\"], emitAdmin);\n } else if (colNameLower === \"content\" || colNameLower === \"body\") {\n // Inside `admin`, because that is where both options live. At the\n // top of the property — where these were — the generated file\n // does not compile: `StringProperty` declares neither. Six of\n // OpenStreetMap's tables have a `body` column, which is how this\n // surfaced.\n extra = withAdminOptions(extra, [\"multiline: true\", \"markdown: true\"], emitAdmin);\n } else if (col.data_type === \"text\") {\n extra = withAdminOptions(extra, [\"multiline: true\"], emitAdmin);\n }\n }\n\n // Append inference results\n if (inferenceExtra) {\n extra += inferenceExtra;\n if (!extra.endsWith(\",\")) extra += \",\";\n }\n\n // ── Rules the database already enforces ──────────────────────────────\n // Everything below is read from the catalog, not guessed from the data\n // or the column's name. Each one is a constraint a write would hit\n // anyway; surfacing it means the form says no before the database does.\n const declaredValidation: string[] = [];\n\n // `varchar(n)` — a bound the author wrote down and nothing has read.\n if (finalPropType === \"string\" &&\n typeof col.character_maximum_length === \"number\" &&\n col.character_maximum_length > 0 &&\n !hasGeneratedKey(extra, \"max\")) {\n declaredValidation.push(`max: ${col.character_maximum_length}`);\n }\n\n if (columnChecks) {\n if (finalPropType === \"number\") {\n if (columnChecks.min !== undefined && !hasGeneratedKey(extra, \"min\")) declaredValidation.push(`min: ${columnChecks.min}`);\n if (columnChecks.max !== undefined && !hasGeneratedKey(extra, \"max\")) declaredValidation.push(`max: ${columnChecks.max}`);\n if (columnChecks.moreThan !== undefined) declaredValidation.push(`moreThan: ${columnChecks.moreThan}`);\n if (columnChecks.lessThan !== undefined) declaredValidation.push(`lessThan: ${columnChecks.lessThan}`);\n }\n if (finalPropType === \"string\") {\n if (columnChecks.lengthMin !== undefined && !hasGeneratedKey(extra, \"min\")) declaredValidation.push(`min: ${columnChecks.lengthMin}`);\n if (columnChecks.lengthMax !== undefined && !declaredValidation.some((v) => v.startsWith(\"max:\")) && !hasGeneratedKey(extra, \"max\")) {\n declaredValidation.push(`max: ${columnChecks.lengthMax}`);\n }\n }\n }\n\n // A single-column unique index is the same promise `validation.unique`\n // makes. Composite uniqueness is not: it constrains the combination, and\n // marking either column unique on its own would reject valid rows.\n if (singleColumnUniques.has(col.column_name) && !meta.pks.includes(col.column_name)) {\n declaredValidation.push(\"unique: true\");\n }\n\n extra = withValidation(extra, declaredValidation);\n\n // A generated column rejects every write, and a tsvector holds lexeme\n // positions rather than text, so an editable field for either is a field\n // that can only ever produce an error.\n if (isReadOnlyColumn(col) && !hasGeneratedKey(extra, \"readOnly\")) {\n const options = [\"readOnly: true\"];\n if (isDerivedIndexColumn(col) && !hasGeneratedKey(extra, \"hideFromCollection\")) {\n options.push(\"hideFromCollection: true\");\n hiddenFromCollection.add(propertyKey);\n }\n extra = withAdminOptions(extra, options, emitAdmin);\n }\n\n // `COMMENT ON COLUMN` — documentation the author already wrote, which\n // introspection has never carried across.\n const columnComment = columnComments.get(col.column_name);\n if (columnComment) {\n extra = `\\n description: ${quote(columnComment)},${extra}`;\n }\n\n // Identify IDs (unless already inferred as UUID/CUID by inferenceEngine)\n if (meta.pks.includes(col.column_name)) {\n if (isCompositePk) {\n extra += `\\n // Part of composite primary key (${commentText(meta.pks.join(\", \"))})`;\n } else if (finalPropType === \"number\" && !inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"increment\\\",\";\n } else if (col.data_type.toLowerCase() === \"uuid\" && !inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"uuid\\\",\";\n } else if (!inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"uuid\\\", // Verify if this is a UUID or CUID\";\n }\n }\n\n if (finalPropType === \"vector\") {\n const dims = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : 1536;\n extra += `\\n dimensions: ${dims},`;\n }\n\n // `required` on a column the user cannot write is a form that cannot be\n // submitted: pagila's `film.fulltext` is NOT NULL and maintained by a\n // trigger, so demanding it of the user blocks every create.\n if (col.is_nullable === \"NO\" && !meta.pks.includes(col.column_name) && !col.column_default && !isReadOnlyColumn(col)) {\n if (extra.includes(\"validation: {\")) {\n extra = extra.replace(\"validation: {\", \"validation: {\\n required: true,\");\n } else {\n extra += \"\\n validation: {\\n required: true\\n },\";\n }\n }\n\n const humanName = humanize(col.column_name);\n\n orderEntries.push({\n key: propertyKey,\n ctx: {\n propType: finalPropType,\n isPk: meta.pks.includes(col.column_name),\n isEnum: isEnumColumn,\n isStorage: extra.includes(\"storage: {\") || inferenceExtra.includes(\"storage: {\"),\n pgDataType: col.data_type,\n originalIndex: currentIndex\n }\n });\n\n propertyBlocks.set(propertyKey, `\n ${propKey(propertyKey)}: {\n name: ${quote(humanName)},\n columnName: ${quote(col.column_name)},\n type: ${quote(finalPropType)},${extra}\n },`);\n }\n\n // Map Owning Relations (from this table's FKs to other tables)\n for (const fk of meta.fks) {\n const targetTableName = fk.foreign_table_name;\n if (!joinTables.has(targetTableName)) {\n // The relation gets its own property key, and it must not be one this\n // file has already used — a duplicate key in an object literal is a\n // TypeScript error, so the whole collection stops compiling.\n //\n // The collision needs three things at once and is invisible without\n // all three: a foreign key column that does *not* end in `_id`, that\n // column also being part of the primary key (which is what keeps it\n // as a property of its own rather than folding it into the relation),\n // and the stripped name matching the target table. MusicBrainz names\n // every foreign key after the table it points at — `area_tag (area,\n // tag)` — so 67 of its 339 collections came out with a property\n // declared twice.\n //\n // Camel-cased for the same reason the columns above are: this is a\n // property key, it sits in the same object literal, and a\n // `blog_author` beside a `publishedAt` is the two-conventions\n // defect reproduced inside a single collection.\n const stripped = toWireKey(fk.column_name.replace(/_id$/, \"\"));\n const relName = firstFreeKey(\n [\n stripped,\n toWireKey(targetTableName),\n `${stripped}Relation`\n ],\n propertyBlocks\n );\n // Push the relation property key, not the FK column name\n orderEntries.push({\n key: relName,\n ctx: {\n propType: \"relation\",\n isPk: false,\n isEnum: false,\n isStorage: false,\n pgDataType: \"\",\n originalIndex: columnIndex++\n }\n });\n\n const targetCollectionCamel = importCollection(targetTableName);\n\n const relHumanName = humanize(relName);\n\n propertyBlocks.set(relName, `\n ${propKey(relName)}: {\n name: ${quote(relHumanName)},\n type: \"relation\",\n // mapped from foreign key: ${commentText(fk.column_name)} -> ${commentText(targetTableName)}(${commentText(fk.foreign_column_name)})\n relation: {\n kind: \"belongsTo\",\n target: ${relationTarget(targetCollectionCamel)},\n localKey: ${quote(fk.column_name)}\n }\n },`);\n }\n }\n\n // Map Inverse Relations (1-to-many where OTHER table points to THIS table)\n // These go into the `relations` array so they render as subcollection tabs.\n const inverseFks = allFks.filter((fk) => fk.foreign_table_name === tableName && !joinTables.has(fk.table_name));\n for (const fk of inverseFks) {\n const sourceTableName = fk.table_name;\n\n const targetCollectionCamel = importCollection(sourceTableName);\n\n relationsOutput += `\n {\n kind: \"hasMany\",\n relationName: ${quote(sourceTableName)},\n target: ${relationTarget(targetCollectionCamel)},\n // the ${commentText(sourceTableName)}.${commentText(fk.column_name)} FK points back here\n foreignKeyOnTarget: ${quote(fk.column_name)}\n },`;\n }\n\n // Map Many-to-Many Relations (Join Tables)\n // These also go into the `relations` array so they render as subcollection tabs.\n const relatedJoinTables = Array.from(joinTables).filter((jt) => {\n const jtMeta = tablesMap.get(jt);\n return jtMeta ? jtMeta.fks.some((fk) => fk.foreign_table_name === tableName) : false;\n });\n\n for (const jt of relatedJoinTables) {\n const jtMeta = tablesMap.get(jt);\n if (!jtMeta) continue;\n\n const joinFks = jtMeta.fks;\n\n // Handle self-referencing M2M: both FKs point to the same table\n const selfRefFks = joinFks.filter((fk) => fk.foreign_table_name === tableName);\n if (selfRefFks.length === 2) {\n // Self-referencing M2M — generate a single owning relation\n const thisFk = selfRefFks[0];\n const otherFk = selfRefFks[1];\n\n const relPropName = `${tableName}_via_${otherFk.column_name.replace(/_id$/, \"\")}`;\n\n relationsOutput += `\n {\n kind: \"manyToMany\",\n relationName: ${quote(relPropName)},\n target: ${relationTarget(toCollectionVarName(tableName))},\n through: {\n table: ${quote(jt)},\n sourceColumn: ${quote(thisFk.column_name)},\n targetColumn: ${quote(otherFk.column_name)}\n }\n },`;\n continue;\n }\n\n const otherFk = joinFks.find((fk) => fk.foreign_table_name !== tableName);\n\n if (otherFk) {\n const targetTableName = otherFk.foreign_table_name;\n\n const targetCollectionCamel = importCollection(targetTableName);\n\n // Both sides of a many-to-many are `manyToMany`. There is no owning\n // and inverse side to pick between any more, so this no longer\n // guesses one from table-name ordering and no longer emits a\n // half-configured relation on the losing side with a comment asking\n // the reader to finish it by hand. Introspection already knows both\n // junction columns; each side just names them from its own end.\n const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);\n\n const throughCode = thisFk\n ? `\\n through: {\\n table: ${quote(jt)},\\n sourceColumn: ${quote(thisFk.column_name)},\\n targetColumn: ${quote(otherFk.column_name)}\\n }`\n : \"\";\n\n relationsOutput += `\n {\n kind: \"manyToMany\",\n relationName: ${quote(targetTableName)},\n target: ${relationTarget(targetCollectionCamel)},${throughCode}\n },`;\n }\n }\n\n const relationsBlock = relationsOutput\n ? `\\n relations: [${relationsOutput}\\n ],`\n : \"\";\n\n const sortedPropertiesOrder = sortPropertiesOrder(orderEntries);\n for (const key of sortedPropertiesOrder) {\n propsOutput += propertyBlocks.get(key) || \"\";\n }\n\n // ── The admin block ──────────────────────────────────────────────────\n // `icon` and `propertiesOrder` used to be emitted at the *top level* of the\n // config, where they have not belonged since the admin block was split out:\n // `PostgresCollectionConfig` does not declare them, so every generated file\n // was a type error, and the panel — which reads the block — never saw them.\n const adminEntries: string[] = [`icon: ${quote(icon)}`];\n\n if (classification) {\n const derivedFacts = context.metadata\n ? buildColumnFacts(meta, context.metadata, enumMap, checkFacts)\n : undefined;\n\n if (classification.role === \"owned-child\") {\n // The rows are already reachable: every inbound foreign key renders\n // as a tab on the parent. A second, top-level entry for them is what\n // turns a navigation of eight nouns into a list of thirty tables.\n adminEntries.push(\"hideFromNavigation: true\");\n } else if (classification.role === \"lookup\") {\n adminEntries.push('group: \"Reference\"');\n }\n\n if (derivedFacts) {\n // Each of these comes back as a column and is emitted as a property.\n const asProperty = (column: string): string => keyByColumn.get(column) ?? toWireKey(column);\n\n const titleProperty = deriveTitleProperty(derivedFacts);\n if (titleProperty) adminEntries.push(`display: { title: ${quote(asProperty(titleProperty))} }`);\n\n const kanbanProperty = deriveKanbanProperty(derivedFacts);\n if (kanbanProperty) adminEntries.push(`kanban: {\\n columnProperty: ${quote(asProperty(kanbanProperty))}\\n }`);\n\n const sort = deriveSort(derivedFacts);\n if (sort) adminEntries.push(`sort: [${quote(asProperty(sort[0]))}, \"desc\"]`);\n }\n\n const listProperties = deriveListProperties(sortedPropertiesOrder, hiddenFromCollection);\n if (listProperties) {\n adminEntries.push(`listProperties: ${formatKeyList(listProperties)}`);\n }\n }\n\n adminEntries.push(`propertiesOrder: ${formatKeyList(sortedPropertiesOrder)}`);\n\n // Every entry above names a property key, and with `defineCollection` those\n // keys are now checked against `properties` — a stale `propertiesOrder` entry\n // left behind by a renamed column is a compile error rather than a silent\n // no-op. Which is also why the block cannot be emitted where the field is not\n // declared: see {@link CollectionBuilder}.\n const adminBlock = emitAdmin\n ? `\\n admin: {\\n ${adminEntries.join(\",\\n \")}\\n }`\n : \"\";\n\n const descriptionBlock = tableComment\n ? `\\n description: ${quote(tableComment)},`\n : \"\";\n\n // The classification is stated in the file because it is a *decision*, and a\n // decision the reader may disagree with. Naming the evidence tells them\n // which line to delete when they do.\n const classificationNote = classification && classification.role !== \"entity\"\n ? `\\n// Introspected as a ${commentText(classification.role)}: ${commentText(classification.reason)}.\\n`\n : \"\";\n\n const collectionVarName = toCollectionVarName(tableName);\n // `const x = defineCollection({ … })` is also the shape the ts-morph schema\n // editor in `@rebasepro/server` expects — `COLLECTION_FACTORIES` — so an\n // introspected collection is now editable from the panel the way a scaffolded\n // one is.\n const [open, close] = builder === \"annotation\"\n ? [`const ${collectionVarName}: PostgresCollectionConfig = {`, \"};\"]\n : [`const ${collectionVarName} = defineCollection({`, \"});\"];\n // Package imports first, then siblings. `AnyCollectionConfig` is added the\n // moment the first relation needs it — which is after the sibling collections\n // it points at have already been added — and only when a relation needs it, so\n // a project with `noUnusedLocals` never sees an import it does not use.\n const importLines = Array.from(imports);\n const orderedImports = [\n ...importLines.filter((line) => !line.includes('from \"./')),\n ...importLines.filter((line) => line.includes('from \"./'))\n ];\n const fileContent = `${orderedImports.join(\"\\n\")}\n${classificationNote}\n${open}\n name: ${quote(collectionName)},\n singularName: ${quote(singular)},\n slug: ${quote(tableName)},\n table: ${quote(tableName)},${descriptionBlock}\n properties: {${propsOutput}\n },${relationsBlock}${adminBlock}\n${close}\n\nexport default ${collectionVarName};\n`;\n\n return fileContent;\n}\n\n/**\n * Generate the content for an index.ts file that re-exports all collections.\n */\nexport function generateIndexContent(fileNames: string[]): string {\n const sorted = [...fileNames].sort();\n let imports = \"\";\n let arrayElements = \"\";\n for (const f of sorted) {\n const varName = toCollectionVarName(f);\n imports += `import ${varName} from ${quote(`./${f}`)};\\n`;\n arrayElements += ` ${varName},\\n`;\n }\n return `${imports}\\nexport const collections = [\\n${arrayElements}];\\n`;\n}\n\n/**\n * Merge new exports into existing index.ts content.\n * Returns the merged content string.\n */\nexport function mergeIndexContent(existingContent: string, newFileNames: string[]): string {\n const existingImports = new Set(\n [...existingContent.matchAll(/import\\s+([a-zA-Z0-9_]+)\\s+from\\s+\"\\.\\/([^\"]+)\"/g)].map((m) => m[2])\n );\n const sorted = [...newFileNames].sort();\n\n let newImports = \"\";\n let newElements = \"\";\n\n for (const f of sorted) {\n if (!existingImports.has(f)) {\n const varName = toCollectionVarName(f);\n newImports += `import ${varName} from ${quote(`./${f}`)};\\n`;\n newElements += ` ${varName},\\n`;\n }\n }\n\n if (!newImports) return existingContent;\n\n // Simple injection logic:\n // Add new imports below the last import or at the top\n const importRegex = /import\\s+.*?;/g;\n let lastImportMatch;\n let match;\n while ((match = importRegex.exec(existingContent)) !== null) {\n lastImportMatch = match;\n }\n\n let contentWithImports = existingContent;\n if (lastImportMatch) {\n const pos = lastImportMatch.index + lastImportMatch[0].length;\n contentWithImports = existingContent.slice(0, pos) + \"\\n\" + newImports.trimEnd() + existingContent.slice(pos);\n } else {\n contentWithImports = newImports + \"\\n\" + existingContent;\n }\n\n // Inject into the `collections = [...]` array\n const arrayRegex = /export\\s+const\\s+collections\\s*=\\s*\\[([\\s\\S]*?)\\];/;\n return contentWithImports.replace(arrayRegex, (fullMatch, arrayContent) => {\n let mergedArray = arrayContent.trimEnd();\n if (mergedArray && !mergedArray.endsWith(\",\")) mergedArray += \",\";\n if (mergedArray) mergedArray += \"\\n\";\n mergedArray += newElements.trimEnd();\n return `export const collections = [\\n ${mergedArray.trim()}\\n];`;\n });\n}\n\n/**\n * Safely extract the host portion of a database URL for logging.\n */\nexport function safeHostFromUrl(url: string): string {\n return url.includes(\"@\") ? url.split(\"@\")[1] : \"(local connection)\";\n}\n"],"mappings":";;;;;;;;;;;;;AAUA,SAAgB,SAAS,WAA2B;CAChD,OAAO,UACF,QAAQ,MAAM,GAAG,CAAC,CAClB,QAAQ,UAAU,MAAM,EAAE,YAAY,CAAC;AAChD;;;ACPA,IAAM,iBAAiB;AACvB,IAAM,aAAa;AACnB,IAAM,aAAa;AAGnB,SAAgB,sBACZ,YACA,YACA,iBACA,cACA,MAOA,YAAY,MACG;CACf,MAAM,SAA0B,CAAC;CACjC,MAAM,aAAuB,CAAC;CAI9B,MAAM,eAAyB,CAAC;CAGhC,MAAM,cAAc,aAAa,QAAO,MAAK,MAAM,QAAQ,MAAM,KAAA,KAAa,MAAM,EAAE;CAEtF,IAAI,YAAY,WAAW,GACvB,OAAO;CAGX,MAAM,eAAe,WAAW,YAAY;CAG5C,IAAI,oBAAoB,UAAU;EAG9B,IADqB,YAAY,OAAM,MAAK,MAAM,KAAK,MAAM,KAAK,MAAM,OAAO,MAAM,GACjF,GAAc;GACd,OAAO,WAAW;GAClB,OAAO;EACX;EAGA,MAAM,YAAY,YAAY,KAAI,MAAK,OAAO,CAAC,CAAC,CAAC,CAAC,QAAO,MAAK,CAAC,MAAM,CAAC,CAAC;EACvE,IAAI,UAAU,WAAW,YAAY,QAAQ;GACzC,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS;GACjC,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS;GAEjC,IAAI,OAAO,KAAK,OAAO,QAAQ,aAAa,SAAS,SAAS,KAAK,aAAa,SAAS,MAAM,KAAK,aAAa,SAAS,OAAO,IAC7H,WAAW,KAAK,6FAA6F;QAC1G,IAAI,OAAO,MAAM,aAAa,SAAS,OAAO,KAAK,aAAa,SAAS,OAAO,KAAK,aAAa,SAAS,QAAQ,IACtH,WAAW,KAAK,kEAAkE;EAE1F;CAOJ;CAGA,IAAI,oBAAoB,SAAS,WAAW,SAAS,MAAM,GAAG;EAE1D,IAAI,YAAY;EAChB,IAAI,aAAa;EAEjB,KAAK,MAAM,KAAK,aAAa;GACzB,IAAI,SAAS;GACb,IAAI,OAAO,MAAM,UACb,IAAI;IAAE,SAAS,KAAK,MAAM,CAAC;GAAG,SAAS,GAAG;IAAE,YAAY;IAAO,aAAa;IAAO;GAAO;GAE9F,IAAI,MAAM,QAAQ,MAAM,GACpB,aAAa;QACV,IAAI,OAAO,WAAW,YAAY,WAAW,MAChD,YAAY;QACT;IACH,YAAY;IACZ,aAAa;GACjB;EACJ;EAEA,IAAI,aAAa,CAAC,YAAY;GAC1B,OAAO,WAAW;GAElB,IAAI,aAAa;GACjB,IAAI,aAAa;GACjB,KAAK,MAAM,KAAK,aAAa;IACzB,MAAM,SAAS,OAAO,MAAM,WAAW,KAAK,MAAM,CAAC,IAAI;IACvD,KAAK,MAAM,QAAQ,QAAQ;KACvB,IAAI,OAAO,SAAS,UAAU,aAAa;KAC3C,IAAI,OAAO,SAAS,UAAU,aAAa;IAC/C;GACJ;GACA,MAAM,YAAY,aAAa,WAAW,aAAa,WAAW;GAClE,WAAW,KAAK,4BAA4B,SAAS,UAAU,EAAE,iBAAiB,UAAU,IAAI;EACpG,OAAO;GACH,OAAO,WAAW;GAGlB,IAAI,cAAc,YAAY,SAAS,GAAG;IACtC,MAAM,SAAiC,CAAC;IACxC,KAAK,MAAM,KAAK,aAAa;KACzB,MAAM,SAAS,OAAO,MAAM,WAAW,KAAK,MAAM,CAAC,IAAI;KACvD,KAAK,MAAM,CAAC,GAAG,QAAQ,OAAO,QAAQ,MAAM,GAAG;MAC3C,IAAI,QAAQ,QAAQ,QAAQ,KAAA,GAAW;MACvC,MAAM,OAAO,OAAO;MACpB,IAAI,SAAS,YAAY,SAAS,YAAY,SAAS;WAC/C,CAAC,OAAO,IAAI,OAAO,KAAK;YACvB,IAAI,OAAO,OAAO,MAAM,OAAO,KAAK;MAAA,OACtC,IAAI,MAAM,QAAQ,GAAG;WACpB,CAAC,OAAO,IAAI,OAAO,KAAK;YACvB,IAAI,OAAO,OAAO,SAAS,OAAO,KAAK;MAAA,OACzC,IAAI,SAAS;WACZ,CAAC,OAAO,IAAI,OAAO,KAAK;YACvB,IAAI,OAAO,OAAO,OAAO,OAAO,KAAK;MAAA;KAElD;IACJ;IAEA,MAAM,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,QAAO,MAAK,OAAO,OAAO,OAAO;IAClE,IAAI,KAAK,SAAS,GAAG;KACjB,MAAM,QAAQ,KAAK,KAAI,MAAK;MACxB,OAAO,qBAAqB,EAAE,aAAa,SAAS,CAAC,EAAE,YAAY,OAAO,GAAG;KACjF,CAAC,CAAC,CAAC,KAAK,GAAG;KACX,WAAW,KAAK,4BAA4B,MAAM,gBAAgB;IACtE,OACI,WAAW,KAAK,4BAA4B;GAEpD,OACI,WAAW,KAAK,4BAA4B;EAEpD;CACJ;CAGA,IAAI,oBAAoB,UAAU;EAE9B,IAAI,YAAY,OAAM,MAAK,OAAO,MAAM,YAAY,eAAe,KAAK,CAAC,CAAC,GAAG;GACzE,OAAO,WAAW;GAClB,OAAO;EACX;EAGA,MAAM,eAAe,IAAI,IAAI,WAAW;EAExC,IAAI,gBAAgB;EACpB,KAAK,MAAM,KAAK,cACZ,IAAI,OAAO,MAAM,YAAY,EAAE,SAAS,eACpC,gBAAgB,EAAE;EAK1B,IAAI,aAAa,OAAO,KAAK,aAAa,QAAQ,KAAK,iBAAiB,MAAM,YAAY,SAAS,aAAa,QAAQ,CAAC,aAAa,IAAI,EAAE;OAEpI,EADe,QAAQ,aAAa,SAAS,KAAK,IACrC;IACb,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,CAAC,KAAI,MAAK,SAAS,KAAK,UAAU,CAAC,EAAE,WAAW,KAAK,UAAU,SAAS,CAAW,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI;IAChJ,WAAW,KAAK,sBAAsB,YAAY,EAAE;IACpD,OAAO,QAAQ,WAAW,SAAS,IAAI,OAAO,WAAW,KAAK,KAAK,IAAI,MAAM;IAC7E,OAAO;GACX;;EAIJ,MAAM,UAAU,YAAY,OAAM,MAAK,OAAO,MAAM,YAAY,WAAW,KAAK,CAAC,CAAC;EAClF,MAAM,UAAU,YAAY,OAAM,MAAK,OAAO,MAAM,YAAY,WAAW,KAAK,CAAC,CAAC;EAClF,IAAI;OACI,MAAM,WAAW,KAAK,4BAA4B;EAAA,OACnD,IAAI;OACH,MAAM,WAAW,KAAK,4BAA4B;EAAA;EAS1D,IAAI,YAAY;EAChB,IAAI,cAAc;EAClB,IAAI,cAAc;EAClB,KAAK,MAAM,KAAK,aACZ,IAAI,OAAO,MAAM,UAAU;GACvB,IAAI,EAAE,SAAS,WAAW,YAAY,EAAE;GACxC,IAAI,EAAE,SAAS,IAAI,GAAG,cAAc;GACpC,IAAI,gBAAgB,KAAK,CAAC,KAAK,aAAa,KAAK,CAAC,KAAK,eAAe,KAAK,CAAC,KAAK,iBAAiB,KAAK,CAAC,GACpG,cAAc;EAEtB;EAOJ,IAAI,aACA,aAAa,KAAK,mBAAmB,gBAAgB;OAClD,IAAI,eAAe,YAAY,KAClC,aAAa,KAAK,iBAAiB;EAGvC,IAAI,YAAY,KAAK,YAAY,KAAO;GAEpC,MAAM,YAAY,KAAK,KAAM,YAAY,MAAO,EAAE,IAAI;GAEtD,IAAI,YAAY,KACZ,WAAW,KAAK,mDAAmD,UAAU,gBAAgB;EAErG;EAGA,MAAM,QAAQ,aAAa,SAAS,MAAM,KAAK,aAAa,SAAS,MAAM,KAAK,aAAa,SAAS,OAAO;EAC7G,MAAM,UAAU,aAAa,SAAS,OAAO,KAAK,aAAa,SAAS,QAAQ,KAAK,aAAa,SAAS,OAAO,KAAK,aAAa,SAAS,MAAM,KAAK,aAAa,SAAS,OAAO;EAGrL,IADwB,YAAY,OAAM,MAAK,OAAO,MAAM,aAAa,EAAE,WAAW,SAAS,KAAK,EAAE,WAAW,UAAU,EACvH,GAEA,IADgB,YAAY,MAAK,MAAK,OAAO,MAAM,YAAY,EAAE,MAAM,gCAAgC,CACnG,KAAW,SAAS;GACpB,WAAW,KAAK,uBAAuB;GACvC,aAAa,KAAK,uBAAuB;EAC7C,OACI,WAAW,KAAK,uBAAuB;OAI3C,IADyB,YAAY,MAAK,MAAK,OAAO,MAAM,YAAY,EAAE,MAAM,iBAAiB,CAC7F,GAAkB;GAClB,MAAM,WAAW,YAAY;GAC7B,MAAM,YAAY,SAAS,YAAY,GAAG;GAC1C,MAAM,sBAAsB,YAAY,IAAI,SAAS,UAAU,GAAG,SAAS,IAAI;GAC/E,WAAW,KAAK,yDAAyD,oBAAoB,iBAAiB;EAClH,OAAO,IAAI,OAAO;GACd,WAAW,KAAK,uBAAuB;GACvC,IAAI,SAAS,aAAa,KAAK,uBAAuB;EAC1D;CAER;CAEA,IAAI,aAAa,aAAa,SAAS,GACnC,WAAW,KAAK,yBAAyB,aAAa,KAAK,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,KAAK,EAAE,gBAAgB;CAGzH,OAAO,QAAQ,WAAW,SAAS,IAAI,OAAO,WAAW,KAAK,KAAK,IAAI,MAAM;CAC7E,OAAO;AACX;;;;;;;;;;;;;ACnPA,SAAgB,UAAU,UAA0B;CAChD,MAAM,KAAK,SAAS,YAAY;CAGhC,IAAI,OAAO,YAAY,OAAO;CAG9B,IAAI,OAAO,WAAW,GAAG,WAAW,GAAG,GAAG,OAAO;CAGjD,IACI,GAAG,SAAS,KAAK,KACjB,GAAG,SAAS,SAAS,KACrB,GAAG,SAAS,SAAS,KACrB,GAAG,SAAS,QAAQ,KACpB,OAAO,UACP,OAAO,YACP,OAAO,YACP,OAAO,sBACP,OAAO,SAEP,OAAO;CAIX,IAAI,GAAG,SAAS,MAAM,GAAG,OAAO;CAGhC,IAAI,GAAG,SAAS,MAAM,KAAK,GAAG,SAAS,MAAM,GAAG,OAAO;CAGvD,IAAI,OAAO,UAAU,OAAO,SAAS,OAAO;CAG5C,IAAI,OAAO,SAAS,OAAO;CAG3B,IAAI,OAAO,UAAU,OAAO,UAAU,OAAO,aAAa,OAAO,YAAY,OAAO;CAGpF,IAAI,OAAO,QAAQ,OAAO;CAG1B,OAAO;AACX;;;;;;;;;;;ACuFA,SAAgB,gBAAgB,QAA8B;CAC1D,IAAI,UAAU,OAAO,SAAS,MAAM,QAAQ,OAAO;CACnD,MAAM,iBAAiB,OAAO,kBAAkB,GAAA,CAAI,YAAY;CAChE,IAAI,CAAC,eAAe,OAAO;CAC3B,OAAO,yJAAyJ,KAAK,aAAa;AACtL;;AAaA,SAAgB,kBAAkB,QAA8B;CAC5D,OAAO,OAAO,iBAAiB;AACnC;;;;;;;;;;AAWA,SAAgB,qBAAqB,QAA8B;CAC/D,OAAO,OAAO,aAAa,cAAc,OAAO,aAAa;AACjE;;AAGA,SAAgB,iBAAiB,QAA8B;CAC3D,OAAO,kBAAkB,MAAM,KAAK,qBAAqB,MAAM;AACnE;;;;;;;;AASA,SAAgB,gBAAgB,QAA8B;CAC1D,OAAO,UAAU,OAAO,SAAS,MAAM,YACnC,OAAO,OAAO,6BAA6B,YAC3C,OAAO,2BAA2B;AAC1C;;;;;;;;;AAUA,SAAgB,gBAAgB,QAAqB,KAAe,WAAiC;CACjG,IAAI,IAAI,SAAS,OAAO,WAAW,GAAG,OAAO;CAC7C,IAAI,UAAU,IAAI,OAAO,WAAW,GAAG,OAAO;CAC9C,IAAI,gBAAgB,MAAM,GAAG,OAAO;CACpC,IAAI,kBAAkB,MAAM,GAAG,OAAO;CACtC,OAAO;AACX;;;;;;;;;AAsBA,SAAgB,iBAAiB,KAA8C;CAC3E,MAAM,yBAAS,IAAI,IAAkC;CACrD,KAAK,MAAM,MAAM,KAAK;EAIlB,MAAM,MAAM,GAAG,GAAG,WAAW,IAAI,GAAG,mBAAmB,GAAG,GAAG,YAAY,IAAI,GAAG;EAChF,MAAM,WAAW,OAAO,IAAI,GAAG;EAC/B,IAAI,UAAU;GACV,SAAS,QAAQ,KAAK,GAAG,WAAW;GACpC,SAAS,eAAe,KAAK,GAAG,mBAAmB;GACnD;EACJ;EACA,OAAO,IAAI,KAAK;GACZ,MAAM,GAAG,mBAAmB,GAAG,GAAG,WAAW,GAAG,GAAG,YAAY;GAC/D,OAAO,GAAG;GACV,SAAS,CAAC,GAAG,WAAW;GACxB,cAAc,GAAG;GACjB,gBAAgB,CAAC,GAAG,mBAAmB;GACvC,YAAY,GAAG;EACnB,CAAC;CACL;CACA,OAAO,MAAM,KAAK,OAAO,OAAO,CAAC;AACrC;;AAGA,SAAS,iBAAiB,OAAe,KAAe,SAA4C;CAChG,MAAM,OAAmB,CAAC;CAC1B,IAAI,IAAI,SAAS,GAAG,KAAK,KAAK,GAAG;CACjC,KAAK,MAAM,UAAU,SACjB,IAAI,OAAO,eAAe,OAAO,KAAK,KAAK,OAAO,YAAY;CAElE,OAAO;AACX;AAEA,SAAS,cAAc,GAAa,GAAsB;CACtD,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK;CACzB,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK;CAC1B,OAAO,KAAK,OAAO,OAAO,UAAU,UAAU,MAAM,MAAM;AAC9D;AAaA,SAAS,aAAa,UAA0B,QAAiD;CAC7F,MAAM,iBAAiB,iBAAiB,SAAS,GAAG;CACpD,MAAM,qCAAqB,IAAI,IAAoC;CACnE,MAAM,iCAAiB,IAAI,IAAoC;CAE/D,KAAK,MAAM,cAAc,gBAAgB;EACrC,MAAM,WAAW,mBAAmB,IAAI,WAAW,KAAK,KAAK,CAAC;EAC9D,SAAS,KAAK,UAAU;EACxB,mBAAmB,IAAI,WAAW,OAAO,QAAQ;EAEjD,MAAM,UAAU,eAAe,IAAI,WAAW,YAAY,KAAK,CAAC;EAChE,QAAQ,KAAK,UAAU;EACvB,eAAe,IAAI,WAAW,cAAc,OAAO;CACvD;CAEA,OAAO;EACH;EACA;EACA;EACA,SAAS,SAAS;EAClB,WAAW,SAAS;CACxB;AACJ;;;;;;;;;;;;;;;AAgBA,SAAS,iBAAiB,OAAe,SAAsD;CAC3F,MAAM,OAAO,QAAQ,OAAO,IAAI,KAAK;CACrC,IAAI,CAAC,MAAM,OAAO;CAElB,MAAM,cAAc,QAAQ,mBAAmB,IAAI,KAAK,KAAK,CAAC;CAC9D,IAAI,YAAY,WAAW,GAAG,OAAO;CACrC,IAAI,YAAY,MAAM,MAAM,EAAE,QAAQ,WAAW,CAAC,GAAG,OAAO;CAE5D,MAAM,CAAC,OAAO,UAAU;CACxB,IAAI,MAAM,QAAQ,OAAO,OAAO,QAAQ,IAAI,OAAO;CAEnD,KAAK,QAAQ,eAAe,IAAI,KAAK,KAAK,CAAC,EAAA,CAAG,SAAS,GAAG,OAAO;CAEjE,MAAM,OAAO,CAAC,MAAM,QAAQ,IAAI,OAAO,QAAQ,EAAE;CAEjD,IAAI,CADe,iBAAiB,OAAO,KAAK,KAAK,QAAQ,OACxD,CAAA,CAAW,MAAM,QAAQ,cAAc,KAAK,IAAI,CAAC,GAAG,OAAO;CAEhE,MAAM,YAAY,IAAI,IAAI,IAAI;CAE9B,IADgB,KAAK,QAAQ,QAAQ,WAAW,gBAAgB,QAAQ,KAAK,KAAK,SAAS,CACvF,CAAA,CAAQ,SAAS,GAAG,OAAO;CAE/B,OAAO;EACH;EACA,MAAM;EACN,QAAQ,gBAAgB,MAAM,aAAa,MAAM,OAAO,aAAa;EACrE,UAAU;GACN,aAAa,MAAM;GACnB,cAAc,MAAM,QAAQ;GAC5B,aAAa,OAAO;GACpB,cAAc,OAAO,QAAQ;EACjC;CACJ;AACJ;;;;;;;;;;AAWA,SAAS,eAAe,OAAe,SAAsD;CACzF,IAAI,CAAC,kBAAkB,OAAO,OAAO,GAAG,OAAO;CAE/C,MAAM,WAAW,QAAQ,UAAU;CACnC,IAAI,OAAO,aAAa,UAAU,OAAO;CAGzC,IAAI,WAAW,KAAK,WAAA,IAA4B,OAAO;CAGvD,OAAO;EACH;EACA,MAAM;EACN,QAAQ,KAAK,SAAS,iCAJJ,QAAQ,eAAe,IAAI,KAAK,KAAK,CAAC,EAAA,CAAG,OAIQ;CACvE;AACJ;;;;;;;AAQA,SAAS,kBAAkB,OAAe,SAAmC;CACzE,MAAM,OAAO,QAAQ,OAAO,IAAI,KAAK;CACrC,IAAI,CAAC,MAAM,OAAO;CAElB,KAAK,QAAQ,mBAAmB,IAAI,KAAK,KAAK,CAAC,EAAA,CAAG,SAAS,GAAG,OAAO;CACrE,KAAK,QAAQ,eAAe,IAAI,KAAK,KAAK,CAAC,EAAA,CAAG,WAAW,GAAG,OAAO;CAEnE,MAAM,UAAU,KAAK,QAAQ,QAAQ,WAAW,gBAAgB,QAAQ,KAAK,qBAAK,IAAI,IAAI,CAAC,CAAC;CAC5F,IAAI,QAAQ,SAAA,GAAqC,OAAO;CAUxD,OAAO,QAAQ,OAAO,WAAW;EAC7B,MAAM,OAAO,UAAU,OAAO,SAAS;EACvC,OAAO,SAAS,aAAa,SAAS,YAAY,SAAS;CAC/D,CAAC;AACL;;;;;;;;;AAUA,SAAgB,iBAAiB,UAA0B,QAA0C;CACjG,MAAM,UAAU,aAAa,UAAU,MAAM;CAC7C,OAAO,MAAM,KAAK,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,UAAU,kBAAkB,OAAO,OAAO,CAAC;AACxF;;;;;;;;;;;AAYA,SAAS,mBAAmB,OAAe,SAAsD;CAC7F,MAAM,OAAO,QAAQ,OAAO,IAAI,KAAK;CACrC,IAAI,CAAC,MAAM,OAAO;CAElB,MAAM,cAAc,QAAQ,mBAAmB,IAAI,KAAK,KAAK,CAAC;CAC9D,IAAI,YAAY,WAAW,GAAG,OAAO;CACrC,KAAK,QAAQ,eAAe,IAAI,KAAK,KAAK,CAAC,EAAA,CAAG,SAAS,GAAG,OAAO;CAEjE,MAAM,QAAQ,UAAU,MAAM,WAAW;CACzC,IAAI,CAAC,OAAO,OAAO;CAEnB,OAAO;EACH;EACA,MAAM;EACN,QAAQ,cAAc,MAAM,WAAW,aAAa,IAAI,iBAAiB,MAAM,QAAQ,EAAE;EACzF,OAAO;GACH,OAAO,MAAM,WAAW;GACxB,QAAQ,MAAM,WAAW,QAAQ;GACjC,UAAU,MAAM;EACpB;CACJ;AACJ;AAEA,SAAS,iBAAiB,UAAqC;CAC3D,QAAQ,UAAR;EACI,KAAK,kBAAkB,OAAO;EAC9B,KAAK,mBAAmB,OAAO;EAC/B,KAAK,qBAAqB,OAAO;EACjC,KAAK,sBAAsB,OAAO;CACtC;AACJ;AAEA,SAAS,UACL,MACA,aACwE;CACxE,MAAM,eAAe,YAAY,QAAQ,MAAM,EAAE,QAAQ,WAAW,CAAC;CACrE,IAAI,aAAa,WAAW,GAAG,OAAO;CAEtC,MAAM,cAAc,IAAI,IAAI,KAAK,QAAQ,KAAK,MAAM,CAAC,EAAE,aAAa,EAAE,gBAAgB,IAAI,CAAC,CAAC;CAE5F,MAAM,YAAY,aAAa,QAAQ,MAAM,EAAE,eAAe,SAAS;CACvE,IAAI,UAAU,WAAW,GAAG,OAAO;EAAE,YAAY,UAAU;EAAI,UAAU;CAAiB;CAE1F,MAAM,cAAc,aAAa,QAAQ,MAAM,KAAK,IAAI,SAAS,EAAE,QAAQ,EAAE,CAAC;CAC9E,IAAI,YAAY,WAAW,GAAG,OAAO;EAAE,YAAY,YAAY;EAAI,UAAU;CAAkB;CAE/F,MAAM,WAAW,aAAa,QAAQ,MAAM,YAAY,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI;CAClF,IAAI,SAAS,WAAW,GAAG,OAAO;EAAE,YAAY,SAAS;EAAI,UAAU;CAAoB;CAO3F,IAAI,YAAY,WAAW,KAAK,IAAI,UAAU,KAAK,IAAI,SAAS,GAAG;EAC/D,MAAM,UAAU,YAAY,MAAM,MAAM,EAAE,QAAQ,OAAO,KAAK,IAAI,EAAE;EACpE,IAAI,SAAS,OAAO;GAAE,YAAY;GAAS,UAAU;EAAqB;CAC9E;CAEA,OAAO;AACX;;;;;;;;;AAUA,SAAgB,eACZ,UACA,QACgC;CAChC,MAAM,UAAU,aAAa,UAAU,MAAM;CAC7C,MAAM,yBAAS,IAAI,IAAiC;CAEpD,KAAK,MAAM,SAAS,OAAO,KAAK,GAAG;EAC/B,MAAM,iBACF,iBAAiB,OAAO,OAAO,KAC/B,eAAe,OAAO,OAAO,KAC7B,mBAAmB,OAAO,OAAO,KAAK;GAClC;GACA,MAAM;GACN,QAAQ;EACZ;EACJ,OAAO,IAAI,OAAO,cAAc;CACpC;CAEA,OAAO;AACX;AAoBA,SAAgB,iBACZ,MACA,UACA,SACA,YACwB;CACxB,MAAM,YAAY,IAAI,IAAI,KAAK,IAAI,KAAK,OAAO,GAAG,WAAW,CAAC;CAC9D,MAAM,sBAAsB,IAAI,IAC5B,SAAS,QACJ,QAAQ,MAAM,EAAE,eAAe,KAAK,QAAQ,EAAE,aAAa,WAAW,CAAC,CAAC,CACxE,KAAK,MAAM,EAAE,aAAa,EAAE,CACrC;CACA,MAAM,cAAc,WAAW,IAAI,KAAK,IAAI;CAE5C,MAAM,wBAAQ,IAAI,IAAyB;CAC3C,KAAK,MAAM,UAAU,KAAK,SAAS;EAC/B,MAAM,SAAS,OAAO,cAAc,iBAAiB,QAAQ,IAAI,OAAO,QAAQ,IAAI,KAAA;EACpF,MAAM,YAAY,aAAa,IAAI,OAAO,WAAW,CAAC,EAAE;EACxD,MAAM,IAAI,OAAO,aAAa;GAC1B;GACA,MAAM,KAAK,IAAI,SAAS,OAAO,WAAW;GAC1C,MAAM,UAAU,IAAI,OAAO,WAAW;GACtC,eAAe,oBAAoB,IAAI,OAAO,WAAW;GACzD,iBAAiB,gBAAgB,MAAM;GACvC,aAAa,kBAAkB,MAAM;GACrC,YAAY,UAAU;GACtB,UAAU,SAAS,WAAW,UAAU,OAAO,SAAS;EAC5D,CAAC;CACL;CACA,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,oBAAoB,OAAqD;CACrF,MAAM,aAAa,MAAM,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ,MAClD,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,EAAE,eACzB,EAAE,aAAa,YACf,CAAC,EAAE,cACH,EAAE,OAAO,gBAAgB,IAC7B;CACA,IAAI,WAAW,WAAW,GAAG,OAAO,KAAA;CAEpC,MAAM,SAAS,WAAW,MAAM,MAAM,EAAE,aAAa;CACrD,IAAI,QAAQ,OAAO,OAAO,OAAO;CAEjC,MAAM,UAAU,WAAW,QAAQ,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAClE,IAAI,QAAQ,SAAS,KAAK,QAAQ,SAAS,WAAW,QAClD,OAAO,QAAQ,EAAE,CAAC,OAAO;CAG7B,OAAO,WAAW,EAAE,CAAC,OAAO;AAChC;;;;;;;;;;AAWA,SAAgB,qBAAqB,OAAqD;CACtF,KAAK,MAAM,QAAQ,MAAM,OAAO,GAAG;EAC/B,IAAI,KAAK,QAAQ,KAAK,QAAQ,KAAK,aAAa;EAChD,IAAI,CAAC,KAAK,YAAY;EACtB,IAAI,KAAK,OAAO,gBAAgB,MAAM;EACtC,IAAI,KAAK,WAAW,SAAA,KAA8B,KAAK,WAAW,SAAA,IAA4B;EAC9F,OAAO,KAAK,OAAO;CACvB;AAEJ;;;;;;;;AASA,SAAgB,WAAW,OAA+D;CACtF,MAAM,SAAS,MAAM,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ,MAAM,EAAE,eAAe;CACzE,IAAI,OAAO,WAAW,GAAG,OAAO,KAAA;CAChC,OAAO,CAAC,OAAO,EAAE,CAAC,OAAO,aAAa,MAAM;AAChD;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,iBAA2B,yBAA8B,IAAI,IAAI,GAAyB;CAC3H,MAAM,UAAU,gBAAgB,QAAQ,QAAQ,CAAC,OAAO,IAAI,GAAG,CAAC;CAChE,IAAI,QAAQ,UAAA,GAA+B,OAAO,KAAA;CAClD,OAAO,QAAQ,MAAM,GAAA,CAAsB;AAC/C;;;;;;;;;;;ACvhBA,IAAM,sBAA8C;CAChD,QAAQ;CACR,UAAU;CACV,KAAK;CACL,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACP,UAAU;CACV,WAAW;AACf;;;;;;;;;;;;AAaA,IAAM,uBAAmD;CACrD,QAAQ;CAAK,SAAS;CAAK,OAAO;CAAK,QAAQ;CAAK,QAAQ;CAC5D,QAAQ;CAAK,QAAQ;CAAK,SAAS;CAAK,QAAQ;CAAK,SAAS;CAC9D,SAAS;CAAK,SAAS;CAAK,SAAS;CAAK,QAAQ;CAClD,QAAQ;CAAM,OAAO;CAAM,OAAO;AACtC;;AAGA,IAAM,8BAAc,IAAI,IAAI;CACxB;CAAU;CAAU;CAAS;CAAO;CAAQ;CAC5C;CAAa;CAAY;CAAS;CAAU;CAC5C;CAAY;CAAe;CAAc;CACzC;CAAQ;CAAU;CAAW;CAAS;CACtC;CAAa;CAAe;CAAe;CAC3C;CAAY;AAChB,CAAC;AAED,SAAgB,YAAY,MAAsB;CAC9C,MAAM,QAAQ,KAAK,YAAY;CAG/B,IAAI,oBAAoB,QAAQ;EAE5B,MAAM,WAAW,oBAAoB;EACrC,OAAO,KAAK,OAAO,KAAK,EAAE,CAAC,YAAY,IACjC,SAAS,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,SAAS,MAAM,CAAC,IACnD;CACV;CAGA,IAAI,YAAY,IAAI,KAAK,GAAG,OAAO;CAGnC,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,GAEzC,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI;CAE/B,IAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,GACxC,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI;CAE/B,IAAI,qBAAqB,QAErB,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI,qBAAqB;CAEpD,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,KAAK,GAC3H,OAAO,KAAK,MAAM,GAAG,EAAE;CAE3B,IAAI,MAAM,SAAS,KAAK,KAAK,CAAC,MAAM,SAAS,MAAM,GAE/C,OAAO,KAAK,MAAM,GAAG,EAAE;CAE3B,IAAI,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,SAAS,IAAI,KAAK,CAAC,MAAM,SAAS,IAAI,KAAK,CAAC,MAAM,SAAS,IAAI,GAC7F,OAAO,KAAK,MAAM,GAAG,EAAE;CAG3B,OAAO;AACX;;;;;AAMA,SAAgB,oBAAoB,WAA2B;CAC3D,MAAM,QAAQ,UAAU,QAAQ,cAAc,IAAI,WAAmB,OAAO,YAAY,CAAC,IAAI;CAK7F,IAAI,cAAc,KAAK,KAAK,GAAG,OAAO;CACtC,MAAM,YAAY,MAAM,QAAQ,mBAAmB,GAAG;CACtD,OAAO,SAAS,KAAK,SAAS,IAAI,IAAI,cAAc;AACxD;AAEA,SAAgB,gBAAgB,WAA2B;CACvD,MAAM,QAAQ,UAAU,YAAY;CACpC,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,UAAU,KAAK,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CACnL,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,GAAG,OAAO;CACpH,IAAI,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,MAAM,GAAG,OAAO;CAChE,IAAI,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,UAAU,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CACzH,IAAI,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,QAAQ,GAAG,OAAO;CAClE,IAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CAC/D,IAAI,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,GAAG,OAAO;CACrH,IAAI,MAAM,SAAS,cAAc,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,OAAO,GAAG,OAAO;CACnG,IAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,GAAG,OAAO;CACxF,IAAI,MAAM,SAAS,cAAc,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CAClG,IAAI,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,UAAU,GAAG,OAAO;CAChG,OAAO;AACX;AAMA,SAAgB,aAAa,YAAgD;CACzE,MAAM,0BAAU,IAAI,IAAsB;CAC1C,KAAK,MAAM,MAAM,YAAY;EACzB,MAAM,WAAW,QAAQ,IAAI,GAAG,SAAS;EACzC,IAAI,UACA,SAAS,KAAK,GAAG,UAAU;OAE3B,QAAQ,IAAI,GAAG,WAAW,CAAC,GAAG,UAAU,CAAC;CAEjD;CACA,OAAO;AACX;AAIA,SAAgB,eACZ,QACA,SACA,KACA,KACsB;CACtB,MAAM,4BAAY,IAAI,IAAuB;CAC7C,KAAK,MAAM,KAAK,QACZ,UAAU,IAAI,EAAE,YAAY;EACxB,MAAM,EAAE;EACR,SAAS,QAAQ,QAAQ,MAAM,EAAE,eAAe,EAAE,UAAU;EAC5D,KAAK,IAAI,QAAQ,OAAO,GAAG,eAAe,EAAE,UAAU,CAAC,CAAC,KAAK,OAAO,GAAG,WAAW;EAClF,KAAK,IAAI,QAAQ,OAAO,GAAG,eAAe,EAAE,UAAU;CAC1D,CAAC;CAEL,OAAO;AACX;;;;;;;;;;;;;;;AAkBA,SAAgB,mBAAmB,WAAgD;CAC/E,MAAM,6BAAa,IAAI,IAAY;CACnC,KAAK,MAAM,CAAC,WAAW,SAAS,UAAU,QAAQ,GAC9C,IAAI,KAAK,IAAI,WAAW;MACM,KAAK,QAAQ,OAAO,MAC1C,KAAK,IAAI,MAAM,OAAO,GAAG,gBAAgB,EAAE,WAAW,KACtD,EAAE,gBAAgB,QAClB,EAAE,gBAAgB,gBAClB,EAAE,gBAAgB,YAGlB,GACA,WAAW,IAAI,SAAS;CAAA;CAIpC,OAAO;AACX;AAwBA,IAAM,iBAAyC;CAC3C,IAAI;CACJ,MAAM;CACN,KAAK;AACT;AAGA,IAAM,cAAsC;CACxC,MAAM;CACN,OAAO;CACP,OAAO;CACP,cAAc;CACd,aAAa;CACb,UAAU;CACV,SAAS;CACT,SAAS;AACb;AAGA,IAAM,uBAA+C;CACjD,YAAY;CACZ,WAAW;CACX,WAAW;CACX,UAAU;CACV,WAAW;CACX,UAAU;CACV,YAAY;CACZ,aAAa;CACb,aAAa;CACb,UAAU;CACV,WAAW;CACX,OAAO;CACP,eAAe;CACf,OAAO;CACP,cAAc;CACd,QAAQ;AACZ;AAGA,IAAM,mBAA2C;CAC7C,MAAM;CACN,MAAM;CACN,KAAK;CACL,WAAW;CACX,KAAK;CACL,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,MAAM;CACN,UAAU;CACV,OAAO;CACP,UAAU;CACV,OAAO;CACP,YAAY;CACZ,UAAU;AACd;AAGA,IAAM,yBAAiD;CACnD,YAAY;CACZ,WAAW;CACX,eAAe;CACf,aAAa;CACb,YAAY;CACZ,WAAW;CACX,aAAa;CACb,eAAe;CACf,YAAY;CACZ,WAAW;CACX,aAAa;AACjB;AAGA,IAAM,iBAAiB;CAAC;CAAQ;CAAS;AAAO;AAChD,IAAM,kCAAkB,IAAI,IAAI;CAAC;CAAe;CAAW;CAAW;CAAY;CAAY;CAAO;CAAa;AAAO,CAAC;AAC1H,IAAM,qCAAqB,IAAI,IAAI;CAAC;CAAW;CAAQ;CAAQ;CAAU;CAAQ;CAAgB;AAAW,CAAC;AAC7G,IAAM,iBAAiB;CAAC;CAAS;CAAU;CAAS;CAAQ;CAAS;CAAa;CAAU;CAAQ;CAAW;AAAQ;AACvH,IAAM,iCAAiB,IAAI,IAAI;CAAC;CAAY;CAAQ;CAAU;CAAiB;CAAY;CAAW;CAAe;CAAQ;CAAW;CAAc;CAAS;AAAiB,CAAC;;;;;;;;;;;AAYjL,SAAgB,wBACZ,YACA,KACM;CAEN,MAAM,MAAM,WAAW,QAAQ,sBAAsB,OAAO,CAAC,CAAC,YAAY;CAC1E,MAAM,aAAa,IAAI,gBAAgB;CAGvC,IAAI,IAAI,MAEJ,QADmB,eAAe,QACZ,KAAK;CAI/B,MAAM,WAAW,uBAAuB;CACxC,IAAI,aAAa,KAAA,GACb,OAAO,WAAW;CAItB,MAAM,aAAa,YAAY;CAC/B,IAAI,eAAe,KAAA,GACf,OAAO,aAAa;CAIxB,MAAM,aAAa,qBAAqB;CACxC,IAAI,eAAe,KAAA,GACf,OAAO,aAAa;CAIxB,MAAM,kBAAkB,iBAAiB;CACzC,IAAI,oBAAoB,KAAA,GACpB,OAAO,kBAAkB;CAK7B,KAAK,MAAM,WAAW,gBAClB,IAAI,IAAI,SAAS,OAAO,KAAK,QAAQ,SACjC,OAAO,KAAK;CAKpB,IAAI,IAAI,WACJ,OAAO,KAAK;CAEhB,KAAK,MAAM,WAAW,gBAClB,IAAI,IAAI,SAAS,OAAO,GACpB,OAAO,KAAK;CAGpB,IAAI,IAAI,SAAS,MAAM,KAAK,IAAI,SAAS,MAAM,KAAK,IAAI,SAAS,OAAO,GACpE,OAAO,KAAK;CAIhB,IAAI,gBAAgB,IAAI,GAAG,GACvB,OAAO,KAAK;CAIhB,IAAI,mBAAmB,IAAI,GAAG,GAC1B,OAAO,KAAK;CAIhB,IAAI,IAAI,aAAa,OACjB,OAAO,eAAe,IAAI,GAAG,IAAI,MAAM,aAAa,MAAM;CAI9D,IAAI,IAAI,aAAa,SACjB,OAAO,MAAM;CAIjB,IAAI,IAAI,aAAa,YACjB,OAAO,KAAK;CAIhB,IAAI,IAAI,QACJ,OAAO,KAAK;CAEhB,IAAI,IAAI,aAAa,WACjB,OAAO,KAAK;CAEhB,IAAI,IAAI,aAAa,YAAY,IAAI,eAAe,QAEhD,OAAO,KAAK;CAIhB,IAAI,IAAI,aAAa,UACjB,OAAO,KAAK;CAEhB,IAAI,IAAI,aAAa,QAEjB,OAAO,KAAK;CAIhB,IAAI,IAAI,aAAa,YAAY,IAAI,eAAe,QAChD,OAAO,KAAK;CAIhB,OAAO,MAAM;AACjB;AAgBA,SAAgB,oBAAoB,SAAyC;CACzE,OAAO,CAAC,GAAG,OAAO,CAAC,CACd,MAAM,GAAG,MAAM,wBAAwB,EAAE,KAAK,EAAE,GAAG,IAAI,wBAAwB,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAC7F,KAAK,MAAM,EAAE,GAAG;AACzB;;;;;;;;;;;AA4DA,IAAa,sBAAsB;AACnC,IAAa,iBAAiB;AAC9B,IAAa,gBAAgB;;AAe7B,SAAS,eAAe,OAAe,SAA2B;CAC9D,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,MAAM,QAAQ,QAAQ,KAAK,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,KAAK;CACnE,IAAI,MAAM,SAAS,eAAe,GAC9B,OAAO,MAAM,QAAQ,iBAAiB,kBAAkB,MAAM,EAAE;CAEpE,OAAO,GAAG,MAAM,+BAA+B,MAAM;AACzD;;;;;;;;;;AAWA,SAAS,gBAAgB,OAAe,KAAsB;CAC1D,OAAO,IAAI,OAAO,cAAc,IAAI,MAAM,CAAC,CAAC,KAAK,KAAK;AAC1D;;;;;;;;;;AAWA,SAAS,iBAAiB,OAAe,SAAmB,YAAY,MAAc;CAClF,IAAI,CAAC,WAAW,OAAO;CACvB,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,MAAM,QAAQ,QAAQ,KAAK,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,KAAK;CACnE,IAAI,MAAM,SAAS,UAAU,GACzB,OAAO,MAAM,QAAQ,YAAY,aAAa,MAAM,EAAE;CAE1D,OAAO,GAAG,MAAM,0BAA0B,MAAM;AACpD;;AAGA,SAAS,MAAM,OAAuB;CAClC,OAAO,KAAK,UAAU,KAAK;AAC/B;AAEA,IAAM,gBAAgB;;;;;;;;;;;;;;;AAgBtB,SAAS,QAAQ,MAAsB;CACnC,OAAO,cAAc,KAAK,IAAI,IAAI,OAAO,MAAM,IAAI;AACvD;;;;;;;;AASA,SAAS,YAAY,OAAuB;CACxC,OAAO,MAAM,QAAQ,kBAAkB,GAAG;AAC9C;;AAGA,SAAS,cAAc,MAAwB;CAC3C,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,OAAO,MAAM,KAAK,KAAK,MAAM,eAAe,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,EAAE;AACxE;;;;;AAMA,SAAgB,uBACZ,WACA,MACA,QACA,YACA,WACA,SACA,YACA,UAA6B,CAAC,GACxB;CACN,MAAM,iBAAiB,SAAS,SAAS;CACzC,MAAM,WAAW,YAAY,cAAc;CAC3C,MAAM,OAAO,gBAAgB,SAAS;CAEtC,MAAM,iBAAiB,QAAQ,iBAAiB,IAAI,SAAS;CAC7D,MAAM,aAAgC,QAAQ,8BAAc,IAAI,IAAI;CACpE,MAAM,cAAc,WAAW,IAAI,SAAS;CAC5C,MAAM,iCAAiB,IAAI,IAAoB;CAC/C,IAAI;CACJ,KAAK,MAAM,WAAW,QAAQ,UAAU,YAAY,CAAC,GAAG;EACpD,IAAI,QAAQ,eAAe,WAAW;EACtC,IAAI,QAAQ,gBAAgB,MAAM,eAAe,QAAQ;OACpD,eAAe,IAAI,QAAQ,aAAa,QAAQ,OAAO;CAChE;CACA,MAAM,sBAAsB,IAAI,KAC3B,QAAQ,UAAU,WAAW,CAAC,EAAA,CAC1B,QAAQ,MAAM,EAAE,eAAe,aAAa,EAAE,aAAa,WAAW,CAAC,CAAC,CACxE,KAAK,MAAM,EAAE,aAAa,EAAE,CACrC;CAEA,MAAM,UAA6B,QAAQ,WAAW;;CAEtD,MAAM,YAAY,YAAY;CAE9B,MAAM,iBAAoD;EACtD,eAAe,oCAAoC,MAAM,mBAAmB,EAAE;EAC9E,QAAQ,oCAAoC,MAAM,cAAc,EAAE;EAClE,YAAY,4CAA4C,MAAM,aAAa,EAAE;CACjF;CACA,MAAM,0BAAU,IAAI,IAAY,CAAC,eAAe,QAAQ,CAAC;;;;;;;;;;;CAYzD,MAAM,oBAAoB,eAA+B;EACrD,MAAM,UAAU,oBAAoB,UAAU;EAC9C,IAAI,eAAe,WAAW,QAAQ,IAAI,UAAU,QAAQ,QAAQ,MAAM,KAAK,YAAY,EAAE,EAAE;EAC/F,OAAO;CACX;;;;;;;;;;;;;;;;;;CAmBA,MAAM,kBAAkB,kBAAkC;EACtD,IAAI,YAAY,cAAc,OAAO,SAAS;EAC9C,QAAQ,IAAI,4CAA4C,MAAM,aAAa,EAAE,EAAE;EAC/E,OAAO,8BAA8B;CACzC;CAEA,IAAI,cAAc;CAClB,IAAI,kBAAkB;CACtB,MAAM,eAAqC,CAAC;CAC5C,MAAM,iCAAiB,IAAI,IAAoB;;;;;;;;;;;CAW/C,MAAM,8BAAc,IAAI,IAAoB;;CAE5C,MAAM,uCAAuB,IAAI,IAAY;CAC7C,IAAI,cAAc;CAGlB,MAAM,gBAAgB,KAAK,IAAI,SAAS;CAGxC,KAAK,MAAM,OAAO,KAAK,SAAS;EAG5B,IAAI,KAAK,IAAI,MAAM,OAAO,GAAG,gBAAgB,IAAI,WAAW,KAAK,CAAC,KAAK,IAAI,SAAS,IAAI,WAAW,GAAG;EAEtG,MAAM,eAAe;EAgBrB,MAAM,cAAc,aAAa,CAAC,UAAU,IAAI,WAAW,GAAG,IAAI,WAAW,GAAG,cAAc;EAC9F,YAAY,IAAI,IAAI,aAAa,WAAW;EAG5C,MAAM,gBAAgB,QAAQ,IAAI,IAAI,QAAQ;EAC9C,MAAM,eAAe,IAAI,cAAc,kBAAkB,kBAAkB,KAAA;EAC3E,MAAM,iBAAiB,IAAI,aAAa;EAExC,MAAM,WAAW,eAAe,WAAY,iBAAiB,WAAW,UAAU,IAAI,SAAS;EAC/F,IAAI,QAAQ;EAEZ,MAAM,eAAe,IAAI,YAAY,YAAY;EAGjD,IAAI,gBAAgB;EACpB,IAAI,iBAAiB;EAErB,IAAI,CAAC,gBAAgB,cAAc,WAAW,SAAS,GAAG;GACtD,MAAM,SAAS,WAAW,KAAI,MAAK,EAAE,IAAI,YAAY;GACrD,MAAM,WAAW,sBAAsB,IAAI,aAAa,IAAI,WAAW,UAAU,QAAQ,KAAK,IAAI,SAAS,IAAI,WAAW,GAAG,SAAS;GACtI,IAAI,SAAS,UAAU,gBAAgB,SAAS;GAChD,IAAI,SAAS,OAAO,iBAAiB,SAAS;EAClD;EAEA,MAAM,eAAe,aAAa,IAAI,IAAI,WAAW;EAGrD,IAAI,gBAAgB,eAAe;GAC/B,MAAM,cAAc,cACf,KAAK,MAAM,SAAS,MAAM,CAAC,EAAE,WAAW,MAAM,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAC/D,KAAK,IAAI;GACd,SAAS,wBAAwB,YAAY;EACjD,OAAO,IAAI,cAAc,cAAc,CAAC,eAAe,SAAS,OAAO,KAAK,aAAa,UAAU;GAK/F,MAAM,cAAc,aAAa,WAC5B,KAAK,MAAM,SAAS,MAAM,CAAC,EAAE,WAAW,MAAM,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAC/D,KAAK,IAAI;GACd,SAAS,wBAAwB,YAAY;EACjD;EAGA,IAAI,kBAAkB;OACd,iBAAiB,gBAAgB,iBAAiB,aAAa;IAC/D,SAAS;IACT,QAAQ,iBAAiB,OAAO,CAAC,kBAAkB,0BAA0B,GAAG,SAAS;IACzF,qBAAqB,IAAI,WAAW;GACxC,OAAO,IAAI,iBAAiB,gBAAgB,iBAAiB,aAAa;IACtE,SAAS;IACT,QAAQ,iBAAiB,OAAO,CAAC,kBAAkB,0BAA0B,GAAG,SAAS;IACzF,qBAAqB,IAAI,WAAW;GACxC,OAAO,IAAI,IAAI,mBAAmB,IAAI,eAAe,SAAS,OAAO,KAAK,IAAI,eAAe,SAAS,mBAAmB,IAAI;IACzH,SAAS;IACT,QAAQ,iBAAiB,OAAO,CAAC,gBAAgB,GAAG,SAAS;GACjE;;EAIJ,IAAI,kBAAkB,WAAW,CAAC,eAAe,SAAS,OAAO,GAAG;GAChE,IAAI,YAAY;GAChB,IAAI,UAAU;GACd,IAAI,IAAI,SAAS,WAAW,GAAG,GAAG;IAE9B,YAAY,UADK,IAAI,SAAS,UAAU,CAClB,CAAQ;IAC9B,IAAI,cAAc,UAAU,UAAU;SACjC,IAAI,cAAc,UAAU,UAAU,IAAI,aAAa,aAAa,cAAc;SAClF,IAAI,cAAc,WAAW,UAAU;GAChD;GACA,IAAI,SACA,SAAS,6BAA6B,MAAM,OAAO,EAAE;GAEzD,SAAS,6BAA6B,MAAM,GAAG,SAAS,IAAI,WAAW,EAAE,MAAM,EAAE,UAAU,MAAM,SAAS,EAAE;EAChH,OAAO,IAAI,kBAAkB,SAAS,CAAC,eAAe,SAAS,gBAAgB,KAAK,CAAC,eAAe,SAAS,eAAe,GACxH,SAAS;EAIb,IAAI,kBAAkB,YAAY,CAAC,gBAAgB,CAAC,gBAAgB;GAChE,MAAM,QAAQ,aAAa,SAAS,MAAM,KAAK,aAAa,SAAS,MAAM,KAAK,aAAa,SAAS,OAAO;GAG7G,IAFgB,aAAa,SAAS,OAAO,KAAK,aAAa,SAAS,QAAQ,KAAK,aAAa,SAAS,OAAO,KAAK,aAAa,SAAS,MAAM,KAAK,aAAa,SAAS,OAAO,GAGjL,SAAS,0DAA0D,MAAM,GAAG,UAAU,GAAG,IAAI,aAAa,EAAE;QACzG,IAAI,OACP,SAAS;QACN,IAAI,iBAAiB,iBAAiB,iBAAiB,aAAa,iBAAiB,WACxF,QAAQ,iBAAiB,OAAO,CAAC,iBAAiB,GAAG,SAAS;QAC3D,IAAI,iBAAiB,aAAa,iBAAiB,QAMtD,QAAQ,iBAAiB,OAAO,CAAC,mBAAmB,gBAAgB,GAAG,SAAS;QAC7E,IAAI,IAAI,cAAc,QACzB,QAAQ,iBAAiB,OAAO,CAAC,iBAAiB,GAAG,SAAS;EAEtE;EAGA,IAAI,gBAAgB;GAChB,SAAS;GACT,IAAI,CAAC,MAAM,SAAS,GAAG,GAAG,SAAS;EACvC;EAMA,MAAM,qBAA+B,CAAC;EAGtC,IAAI,kBAAkB,YAClB,OAAO,IAAI,6BAA6B,YACxC,IAAI,2BAA2B,KAC/B,CAAC,gBAAgB,OAAO,KAAK,GAC7B,mBAAmB,KAAK,QAAQ,IAAI,0BAA0B;EAGlE,IAAI,cAAc;GACd,IAAI,kBAAkB,UAAU;IAC5B,IAAI,aAAa,QAAQ,KAAA,KAAa,CAAC,gBAAgB,OAAO,KAAK,GAAG,mBAAmB,KAAK,QAAQ,aAAa,KAAK;IACxH,IAAI,aAAa,QAAQ,KAAA,KAAa,CAAC,gBAAgB,OAAO,KAAK,GAAG,mBAAmB,KAAK,QAAQ,aAAa,KAAK;IACxH,IAAI,aAAa,aAAa,KAAA,GAAW,mBAAmB,KAAK,aAAa,aAAa,UAAU;IACrG,IAAI,aAAa,aAAa,KAAA,GAAW,mBAAmB,KAAK,aAAa,aAAa,UAAU;GACzG;GACA,IAAI,kBAAkB,UAAU;IAC5B,IAAI,aAAa,cAAc,KAAA,KAAa,CAAC,gBAAgB,OAAO,KAAK,GAAG,mBAAmB,KAAK,QAAQ,aAAa,WAAW;IACpI,IAAI,aAAa,cAAc,KAAA,KAAa,CAAC,mBAAmB,MAAM,MAAM,EAAE,WAAW,MAAM,CAAC,KAAK,CAAC,gBAAgB,OAAO,KAAK,GAC9H,mBAAmB,KAAK,QAAQ,aAAa,WAAW;GAEhE;EACJ;EAKA,IAAI,oBAAoB,IAAI,IAAI,WAAW,KAAK,CAAC,KAAK,IAAI,SAAS,IAAI,WAAW,GAC9E,mBAAmB,KAAK,cAAc;EAG1C,QAAQ,eAAe,OAAO,kBAAkB;EAKhD,IAAI,iBAAiB,GAAG,KAAK,CAAC,gBAAgB,OAAO,UAAU,GAAG;GAC9D,MAAM,UAAU,CAAC,gBAAgB;GACjC,IAAI,qBAAqB,GAAG,KAAK,CAAC,gBAAgB,OAAO,oBAAoB,GAAG;IAC5E,QAAQ,KAAK,0BAA0B;IACvC,qBAAqB,IAAI,WAAW;GACxC;GACA,QAAQ,iBAAiB,OAAO,SAAS,SAAS;EACtD;EAIA,MAAM,gBAAgB,eAAe,IAAI,IAAI,WAAW;EACxD,IAAI,eACA,QAAQ,8BAA8B,MAAM,aAAa,EAAE,GAAG;EAIlE,IAAI,KAAK,IAAI,SAAS,IAAI,WAAW;OAC7B,eACA,SAAS,mDAAmD,YAAY,KAAK,IAAI,KAAK,IAAI,CAAC,EAAE;QAC1F,IAAI,kBAAkB,YAAY,CAAC,eAAe,SAAS,OAAO,GACrE,SAAS;QACN,IAAI,IAAI,UAAU,YAAY,MAAM,UAAU,CAAC,eAAe,SAAS,OAAO,GACjF,SAAS;QACN,IAAI,CAAC,eAAe,SAAS,OAAO,GACvC,SAAS;EAAA;EAIjB,IAAI,kBAAkB,UAAU;GAC5B,MAAM,OAAO,IAAI,aAAa,IAAI,YAAY,IAAI,IAAI,YAAY;GAClE,SAAS,6BAA6B,KAAK;EAC/C;EAKA,IAAI,IAAI,gBAAgB,QAAQ,CAAC,KAAK,IAAI,SAAS,IAAI,WAAW,KAAK,CAAC,IAAI,kBAAkB,CAAC,iBAAiB,GAAG,GAC/G,IAAI,MAAM,SAAS,eAAe,GAC9B,QAAQ,MAAM,QAAQ,iBAAiB,gDAAgD;OAEvF,SAAS;EAIjB,MAAM,YAAY,SAAS,IAAI,WAAW;EAE1C,aAAa,KAAK;GACd,KAAK;GACL,KAAK;IACD,UAAU;IACV,MAAM,KAAK,IAAI,SAAS,IAAI,WAAW;IACvC,QAAQ;IACR,WAAW,MAAM,SAAS,YAAY,KAAK,eAAe,SAAS,YAAY;IAC/E,YAAY,IAAI;IAChB,eAAe;GACnB;EACJ,CAAC;EAED,eAAe,IAAI,aAAa;UAC9B,QAAQ,WAAW,EAAE;oBACX,MAAM,SAAS,EAAE;0BACX,MAAM,IAAI,WAAW,EAAE;oBAC7B,MAAM,aAAa,EAAE,GAAG,MAAM;WACvC;CACP;CAGA,KAAK,MAAM,MAAM,KAAK,KAAK;EACvB,MAAM,kBAAkB,GAAG;EAC3B,IAAI,CAAC,WAAW,IAAI,eAAe,GAAG;GAkBlC,MAAM,WAAW,UAAU,GAAG,YAAY,QAAQ,QAAQ,EAAE,CAAC;GAC7D,MAAM,UAAU,aACZ;IACI;IACA,UAAU,eAAe;IACzB,GAAG,SAAS;GAChB,GACA,cACJ;GAEA,aAAa,KAAK;IACd,KAAK;IACL,KAAK;KACD,UAAU;KACV,MAAM;KACN,QAAQ;KACR,WAAW;KACX,YAAY;KACZ,eAAe;IACnB;GACJ,CAAC;GAED,MAAM,wBAAwB,iBAAiB,eAAe;GAE9D,MAAM,eAAe,SAAS,OAAO;GAErC,eAAe,IAAI,SAAS;UAC9B,QAAQ,OAAO,EAAE;oBACP,MAAM,YAAY,EAAE;;0CAEE,YAAY,GAAG,WAAW,EAAE,MAAM,YAAY,eAAe,EAAE,GAAG,YAAY,GAAG,mBAAmB,EAAE;;;0BAGtH,eAAe,qBAAqB,EAAE;4BACpC,MAAM,GAAG,WAAW,EAAE;;WAEvC;EACH;CACJ;CAIA,MAAM,aAAa,OAAO,QAAQ,OAAO,GAAG,uBAAuB,aAAa,CAAC,WAAW,IAAI,GAAG,UAAU,CAAC;CAC9G,KAAK,MAAM,MAAM,YAAY;EACzB,MAAM,kBAAkB,GAAG;EAE3B,MAAM,wBAAwB,iBAAiB,eAAe;EAE9D,mBAAmB;;;4BAGC,MAAM,eAAe,EAAE;sBAC7B,eAAe,qBAAqB,EAAE;qBACvC,YAAY,eAAe,EAAE,GAAG,YAAY,GAAG,WAAW,EAAE;kCAC/C,MAAM,GAAG,WAAW,EAAE;;CAEpD;CAIA,MAAM,oBAAoB,MAAM,KAAK,UAAU,CAAC,CAAC,QAAQ,OAAO;EAC5D,MAAM,SAAS,UAAU,IAAI,EAAE;EAC/B,OAAO,SAAS,OAAO,IAAI,MAAM,OAAO,GAAG,uBAAuB,SAAS,IAAI;CACnF,CAAC;CAED,KAAK,MAAM,MAAM,mBAAmB;EAChC,MAAM,SAAS,UAAU,IAAI,EAAE;EAC/B,IAAI,CAAC,QAAQ;EAEb,MAAM,UAAU,OAAO;EAGvB,MAAM,aAAa,QAAQ,QAAQ,OAAO,GAAG,uBAAuB,SAAS;EAC7E,IAAI,WAAW,WAAW,GAAG;GAEzB,MAAM,SAAS,WAAW;GAC1B,MAAM,UAAU,WAAW;GAE3B,MAAM,cAAc,GAAG,UAAU,OAAO,QAAQ,YAAY,QAAQ,QAAQ,EAAE;GAE9E,mBAAmB;;;4BAGH,MAAM,WAAW,EAAE;sBACzB,eAAe,oBAAoB,SAAS,CAAC,EAAE;;yBAE5C,MAAM,EAAE,EAAE;gCACH,MAAM,OAAO,WAAW,EAAE;gCAC1B,MAAM,QAAQ,WAAW,EAAE;;;GAG/C;EACJ;EAEA,MAAM,UAAU,QAAQ,MAAM,OAAO,GAAG,uBAAuB,SAAS;EAExE,IAAI,SAAS;GACT,MAAM,kBAAkB,QAAQ;GAEhC,MAAM,wBAAwB,iBAAiB,eAAe;GAQ9D,MAAM,SAAS,QAAQ,MAAM,OAAO,GAAG,uBAAuB,SAAS;GAEvE,MAAM,cAAc,SACd,oDAAoD,MAAM,EAAE,EAAE,mCAAmC,MAAM,OAAO,WAAW,EAAE,mCAAmC,MAAM,QAAQ,WAAW,EAAE,mBACzL;GAEN,mBAAmB;;;4BAGH,MAAM,eAAe,EAAE;sBAC7B,eAAe,qBAAqB,EAAE,GAAG,YAAY;;EAEnE;CACJ;CAEA,MAAM,iBAAiB,kBACjB,qBAAqB,gBAAgB,YACrC;CAEN,MAAM,wBAAwB,oBAAoB,YAAY;CAC9D,KAAK,MAAM,OAAO,uBACd,eAAe,eAAe,IAAI,GAAG,KAAK;CAQ9C,MAAM,eAAyB,CAAC,SAAS,MAAM,IAAI,GAAG;CAEtD,IAAI,gBAAgB;EAChB,MAAM,eAAe,QAAQ,WACvB,iBAAiB,MAAM,QAAQ,UAAU,SAAS,UAAU,IAC5D,KAAA;EAEN,IAAI,eAAe,SAAS,eAIxB,aAAa,KAAK,0BAA0B;OACzC,IAAI,eAAe,SAAS,UAC/B,aAAa,KAAK,sBAAoB;EAG1C,IAAI,cAAc;GAEd,MAAM,cAAc,WAA2B,YAAY,IAAI,MAAM,KAAK,UAAU,MAAM;GAE1F,MAAM,gBAAgB,oBAAoB,YAAY;GACtD,IAAI,eAAe,aAAa,KAAK,qBAAqB,MAAM,WAAW,aAAa,CAAC,EAAE,GAAG;GAE9F,MAAM,iBAAiB,qBAAqB,YAAY;GACxD,IAAI,gBAAgB,aAAa,KAAK,0CAA0C,MAAM,WAAW,cAAc,CAAC,EAAE,YAAY;GAE9H,MAAM,OAAO,WAAW,YAAY;GACpC,IAAI,MAAM,aAAa,KAAK,UAAU,MAAM,WAAW,KAAK,EAAE,CAAC,EAAE,UAAU;EAC/E;EAEA,MAAM,iBAAiB,qBAAqB,uBAAuB,oBAAoB;EACvF,IAAI,gBACA,aAAa,KAAK,mBAAmB,cAAc,cAAc,GAAG;CAE5E;CAEA,aAAa,KAAK,oBAAoB,cAAc,qBAAqB,GAAG;CAO5E,MAAM,aAAa,YACb,2BAA2B,aAAa,KAAK,aAAa,EAAE,WAC5D;CAEN,MAAM,mBAAmB,eACnB,sBAAsB,MAAM,YAAY,EAAE,KAC1C;CAKN,MAAM,qBAAqB,kBAAkB,eAAe,SAAS,WAC/D,0BAA0B,YAAY,eAAe,IAAI,EAAE,IAAI,YAAY,eAAe,MAAM,EAAE,OAClG;CAEN,MAAM,oBAAoB,oBAAoB,SAAS;CAKvD,MAAM,CAAC,MAAM,SAAS,YAAY,eAC5B,CAAC,SAAS,kBAAkB,iCAAiC,IAAI,IACjE,CAAC,SAAS,kBAAkB,wBAAwB,KAAK;CAK/D,MAAM,cAAc,MAAM,KAAK,OAAO;CAmBtC,OAAO,GAdgB,CAHnB,GAAG,YAAY,QAAQ,SAAS,CAAC,KAAK,SAAS,WAAU,CAAC,GAC1D,GAAG,YAAY,QAAQ,SAAS,KAAK,SAAS,WAAU,CAAC,CAEtC,CAAA,CAAe,KAAK,IAAI,EAAE;EACnD,mBAAmB;EACnB,KAAK;YACK,MAAM,cAAc,EAAE;oBACd,MAAM,QAAQ,EAAE;YACxB,MAAM,SAAS,EAAE;aAChB,MAAM,SAAS,EAAE,GAAG,iBAAiB;mBAC/B,YAAY;QACvB,iBAAiB,WAAW;EAClC,MAAM;;iBAES,kBAAkB;;AAInC;;;;AAKA,SAAgB,qBAAqB,WAA6B;CAC9D,MAAM,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK;CACnC,IAAI,UAAU;CACd,IAAI,gBAAgB;CACpB,KAAK,MAAM,KAAK,QAAQ;EACpB,MAAM,UAAU,oBAAoB,CAAC;EACrC,WAAW,UAAU,QAAQ,QAAQ,MAAM,KAAK,GAAG,EAAE;EACrD,iBAAiB,OAAO,QAAQ;CACpC;CACA,OAAO,GAAG,QAAQ,kCAAkC,cAAc;AACtE;;;;;AAMA,SAAgB,kBAAkB,iBAAyB,cAAgC;CACvF,MAAM,kBAAkB,IAAI,IACxB,CAAC,GAAG,gBAAgB,SAAS,kDAAkD,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CACrG;CACA,MAAM,SAAS,CAAC,GAAG,YAAY,CAAC,CAAC,KAAK;CAEtC,IAAI,aAAa;CACjB,IAAI,cAAc;CAElB,KAAK,MAAM,KAAK,QACZ,IAAI,CAAC,gBAAgB,IAAI,CAAC,GAAG;EACzB,MAAM,UAAU,oBAAoB,CAAC;EACrC,cAAc,UAAU,QAAQ,QAAQ,MAAM,KAAK,GAAG,EAAE;EACxD,eAAe,OAAO,QAAQ;CAClC;CAGJ,IAAI,CAAC,YAAY,OAAO;CAIxB,MAAM,cAAc;CACpB,IAAI;CACJ,IAAI;CACJ,QAAQ,QAAQ,YAAY,KAAK,eAAe,OAAO,MACnD,kBAAkB;CAGtB,IAAI,qBAAqB;CACzB,IAAI,iBAAiB;EACjB,MAAM,MAAM,gBAAgB,QAAQ,gBAAgB,EAAE,CAAC;EACvD,qBAAqB,gBAAgB,MAAM,GAAG,GAAG,IAAI,OAAO,WAAW,QAAQ,IAAI,gBAAgB,MAAM,GAAG;CAChH,OACI,qBAAqB,aAAa,OAAO;CAK7C,OAAO,mBAAmB,QAAQ,uDAAa,WAAW,iBAAiB;EACvE,IAAI,cAAc,aAAa,QAAQ;EACvC,IAAI,eAAe,CAAC,YAAY,SAAS,GAAG,GAAG,eAAe;EAC9D,IAAI,aAAa,eAAe;EAChC,eAAe,YAAY,QAAQ;EACnC,OAAO,qCAAqC,YAAY,KAAK,EAAE;CACnE,CAAC;AACL;;;;AAKA,SAAgB,gBAAgB,KAAqB;CACjD,OAAO,IAAI,SAAS,GAAG,IAAI,IAAI,MAAM,GAAG,CAAC,CAAC,KAAK;AACnD"}
|