joist-core 2.4.0-next.45 → 2.4.0-next.46
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/build/drivers/EntityWriter.cjs.map +1 -1
- package/build/drivers/EntityWriter.d.cts +12 -0
- package/build/drivers/EntityWriter.d.cts.map +1 -1
- package/build/drivers/EntityWriter.d.mts +12 -0
- package/build/drivers/EntityWriter.d.mts.map +1 -1
- package/build/drivers/EntityWriter.js.map +1 -1
- package/build/queries/unnest.cjs +4 -5
- package/build/queries/unnest.cjs.map +1 -1
- package/build/queries/unnest.js +4 -5
- package/build/queries/unnest.js.map +1 -1
- package/build/serde/columns.cjs +3 -0
- package/build/serde/columns.cjs.map +1 -1
- package/build/serde/columns.d.cts +1 -0
- package/build/serde/columns.d.cts.map +1 -1
- package/build/serde/columns.d.mts +1 -0
- package/build/serde/columns.d.mts.map +1 -1
- package/build/serde/columns.js +3 -0
- package/build/serde/columns.js.map +1 -1
- package/build/serde/fieldSerde.cjs +3 -0
- package/build/serde/fieldSerde.cjs.map +1 -1
- package/build/serde/fieldSerde.d.cts +1 -0
- package/build/serde/fieldSerde.d.cts.map +1 -1
- package/build/serde/fieldSerde.d.mts +1 -0
- package/build/serde/fieldSerde.d.mts.map +1 -1
- package/build/serde/fieldSerde.js +3 -0
- package/build/serde/fieldSerde.js.map +1 -1
- package/build/serde/serde.cjs +25 -0
- package/build/serde/serde.cjs.map +1 -1
- package/build/serde/serde.d.cts +21 -0
- package/build/serde/serde.d.cts.map +1 -1
- package/build/serde/serde.d.mts +21 -0
- package/build/serde/serde.d.mts.map +1 -1
- package/build/serde/serde.js +25 -0
- package/build/serde/serde.js.map +1 -1
- package/build/serde/temporalMappers.cjs +8 -4
- package/build/serde/temporalMappers.cjs.map +1 -1
- package/build/serde/temporalMappers.d.cts.map +1 -1
- package/build/serde/temporalMappers.d.mts.map +1 -1
- package/build/serde/temporalMappers.js +8 -4
- package/build/serde/temporalMappers.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityWriter.cjs","names":["groupBy","getMetadata","hasSerde","getInstanceData","isChangeableField","getBaseSelfAndSubMetas","keyToNumber","getBaseAndSelfMetas"],"sources":["../../src/drivers/EntityWriter.ts"],"sourcesContent":["import { getInstanceData } from \"src/BaseEntity.ts\";\nimport { type Entity } from \"src/Entity.ts\";\nimport {\n type EntityMetadata,\n type Field,\n type PrimitiveField,\n getBaseAndSelfMetas,\n getBaseSelfAndSubMetas,\n getMetadata,\n} from \"src/EntityMetadata.ts\";\nimport { getField, isChangeableField } from \"src/fields.ts\";\nimport type { Todo } from \"src/flush/Todo.ts\";\nimport { keyToNumber } from \"src/keys.ts\";\nimport { type FieldColumn, type TimestampSerde, hasSerde } from \"src/serde/fieldSerde.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\n/** A simplified view of columns, with only the keys necessary to create SQL statements. */\nexport type OpColumn = { columnName: string; dbType: string; isNullableArray?: boolean };\nexport type InsertOp = { tableName: string; columns: OpColumn[]; columnValues: any[][] };\n/**\n * A logical `update` operation.\n *\n * If we're using op locks, `columns` will include both the new `updatedAt` value (as bumped\n * by the `EntityManager` on mutate), as well as an extra/last \"original updated at\" column\n * (with respective pre-update values in the `rows` data), for including in the conditional\n * update clause.\n */\nexport type UpdateOp = {\n tableName: string;\n columns: OpColumn[];\n columnValues: any[][];\n updatedAt: string | undefined;\n};\nexport type DeleteOp = { tableName: string; ids: any[] };\n\ntype Ops = { inserts: InsertOp[]; updates: UpdateOp[]; deletes: DeleteOp[] };\n\n/**\n * In schemas with entity FK cycles, we may insert a dummy/null value, and later fix it up\n * before the transaction commits.\n *\n * I.e. `INSERT author.favorite_book_id` as `NULL` and then later do an `UPDATE author\n * SET favorite_book_id = 1` after the book has been inserted/we have the id.\n */\nexport type InsertFixup = {\n entity: Entity;\n tableName: string;\n column: OpColumn;\n value: any;\n};\n\n/**\n * Builds AST-ish `Ops` for the inserts/updates/deletes in `todos`.\n *\n * This helps decouple the database-specific driver/library from the knowledge\n * of turning entities into table operations, i.e. so they can be unaware of\n * complexities like field -> column mapping, oplocks, and class table inheritance.\n *\n * We still assume Postgres b/c we assume all ids have been pre-assigned for INSERTs,\n * and don't do any topological sorting of cross-row FK dependencies.\n *\n * But this should let us experiment with knex vs. raw client/etc.\n */\nexport function generateOps(todos: Record<string, Todo>): Ops {\n const ops: Ops = { inserts: [], updates: [], deletes: [] };\n const fixups: InsertFixup[] = [];\n // Would be nice to conditionally sort\n const sorted = Object.values(todos).sort(\n (a, b) => (a.metadata.nonDeferredFkOrder ?? 0) - (b.metadata.nonDeferredFkOrder ?? 0),\n );\n for (const todo of sorted) {\n addInserts(ops, todo, fixups);\n addUpdates(ops, todo);\n addDeletes(ops, todo);\n }\n if (fixups.length > 0) translateFixupsIntoUpdates(ops, fixups);\n return ops;\n}\n\nfunction addInserts(ops: Ops, todo: Todo, fixups: InsertFixup[]): void {\n if (todo.inserts.length > 0) {\n // If we have subtypes, this todo.metadata will always be the base type\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n // Insert into each of the CTI tables\n for (const [meta, group] of groupEntitiesByTable(todo.inserts)) {\n ops.inserts.push(newInsertOp(meta, group, fixups));\n }\n } else if (meta.inheritanceType === \"sti\") {\n // One INSERT per subtype, so each statement's column list spans exactly what that subtype can\n // set. A single statement across subtypes has to bind NULL for the other subtypes' columns,\n // which defeats their database defaults, and outright fails for notNull ones.\n for (const group of groupBy(todo.inserts, (e) => getMetadata(e)).values()) {\n ops.inserts.push(newStiInsertOp(meta, group, fixups));\n }\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n ops.inserts.push(newInsertOp(meta, todo.inserts, fixups));\n }\n }\n}\n\nfunction newInsertOp(meta: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // Purposefully use `meta.fields` instead of `allFields` b/c each CTI table has its own `InsertOp`\n const columns = Object.values(meta.fields)\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, meta.tableName, columns, fixups);\n return { tableName: meta.tableName, columns, columnValues };\n}\n\nfunction newStiInsertOp(root: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // `allFields` is the base's fields plus this subtype's own, i.e. everything this subtype can write.\n // Anything it can't write is left out of the statement entirely, so the database applies its default.\n const fields: Field[] = Object.values(getMetadata(entities[0]).allFields);\n const columns = fields.filter(hasSerde).flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, root.tableName, columns, fixups);\n return { tableName: root.tableName, columns, columnValues };\n}\n\nfunction addUpdates(ops: Ops, todo: Todo): void {\n if (todo.updates.length > 0) {\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n for (const [meta, group] of groupEntitiesByTable(todo.updates)) {\n const op = newUpdateOp(meta, group);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, group);\n }\n } else if (meta.inheritanceType === \"sti\") {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n }\n }\n}\n\n/**\n * Writes changed `lazy` columns via a targeted `UPDATE table SET lazy_col = ... WHERE id in (...)`.\n *\n * Lazy columns are excluded from the main batch UPDATE (which spans the batch's union of changed fields),\n * so that entities in the batch that never loaded a lazy column keep their existing db value instead of\n * being overwritten with null. Here we write each lazy column only for the entities that actually changed it.\n */\nfunction addLazyUpdates(ops: Ops, meta: EntityMetadata, entities: Entity[]): void {\n const idColumn = meta.fields[\"id\"].serde!.columns[0];\n for (const fieldName of meta.lazyFieldNames!) {\n const field = meta.fields[fieldName];\n if (!hasSerde(field)) continue;\n const changed = entities.filter((e) => fieldName in getInstanceData(e).changedData);\n if (changed.length === 0) continue;\n const columns: BindingColumn[] = [idColumn, ...field.serde.columns];\n const columnValues = collectBindings(changed, meta.tableName, columns, undefined);\n ops.updates.push({ tableName: meta.tableName, columns, columnValues, updatedAt: undefined });\n }\n}\n\nfunction newUpdateOp(meta: EntityMetadata, entities: Entity[]): UpdateOp | undefined {\n // We only include changed fields in our `UPDATE`--maybe we could change this\n // to always use the same fields, to take advantage of Prepared Statements.\n const changedFields = new Set<string>();\n for (const entity of entities) {\n for (const fieldName in getInstanceData(entity).changedData) changedFields.add(fieldName);\n }\n // Sometimes with derived fields, an instance will be marked as an update, but if the derived field hasn't\n // actually changed, it'll be a noop, so just short-circuit if it looks like that happened. Unless touched.\n if (changedFields.size === 0 && !entities.some((e) => getInstanceData(e).isTouched)) {\n return undefined;\n }\n\n // We may have loaded a1 and a2, and changed a1.firstName, and a2.lastName, but either one\n // might be missing the other's changed fields in it's lazy-initialized data field...\n // (Use `?` because subtypes won't have the updatedAt, and it will be handled by the base type\n // `newUpdateUp`--except STI where we're doing it all in one go, probably needs checked here.)\n const updatedAt = meta.timestampFields?.updatedAt;\n changedFields.add(\"id\");\n if (updatedAt) changedFields.add(updatedAt);\n // `lazy` columns are deliberately excluded from the batch UPDATE and written by `addLazyUpdates`; an\n // entity that never loaded a lazy column isn't in `row`, so forcing `getField` here would clobber it to null.\n const lazyFieldNames = meta.lazyFieldNames!;\n for (const entity of entities) {\n const { data } = getInstanceData(entity);\n for (const key of changedFields) {\n // Check isChangeableField because we might be updating the base `publishers` table\n // and `originalData` might have fields from a subclass `large_publishers` table.\n if (!(key in data) && isChangeableField(entity, key) && !lazyFieldNames.has(key)) {\n getField(entity, key, true);\n }\n }\n }\n\n const columns: Array<BindingColumn> = (\n meta.inheritanceType === \"sti\"\n ? // Hack this one handling of STI into here...\n getBaseSelfAndSubMetas(meta).flatMap((meta) =>\n meta.stiDiscriminatorField\n ? Object.values(meta.fields)\n : Object.values(meta.fields).filter((f) => f.fieldName !== \"id\"),\n )\n : Object.values(meta.fields)\n )\n .filter((f) => changedFields.has(f.fieldName))\n .filter((f) => !lazyFieldNames.has(f.fieldName))\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n\n // If we're using class table inheritance, base/child tables may not have any columns to update\n if (columns.length === 1) {\n return undefined;\n }\n\n // We already have the bumped updated_at column, but also include the original updated_at for the data CTE\n if (updatedAt) {\n const serde = meta.fields[updatedAt].serde as TimestampSerde<unknown>;\n columns.push({\n columnName: \"__original_updated_at\",\n dbType: \"timestamptz\",\n dbValue: (_, entity) => serde.dbValue(getInstanceData(entity).originalData),\n });\n }\n\n const columnValues = collectBindings(entities, meta.tableName, columns, []);\n const updatedAtColumn = updatedAt ? (meta.fields[updatedAt] as PrimitiveField).serde.columns[0] : undefined;\n return { tableName: meta.tableName, columns, updatedAt: updatedAtColumn?.columnName, columnValues };\n}\n\nfunction addDeletes(ops: Ops, todo: Todo): void {\n if (todo.deletes.length > 0) {\n const meta = todo.metadata;\n const ids = todo.deletes.map((e) => keyToNumber(meta, e.idTagged!).toString());\n if (meta.subTypes.length > 0) {\n getBaseSelfAndSubMetas(meta).forEach((meta) => {\n ops.deletes.push({ tableName: meta.tableName, ids });\n });\n } else {\n ops.deletes.push({ tableName: meta.tableName, ids });\n }\n }\n}\n\nfunction groupEntitiesByTable(entities: Entity[]): Array<[EntityMetadata, Entity[]]> {\n const entitiesByType: Map<EntityMetadata, Entity[]> = new Map();\n for (const e of entities) {\n for (const m of getBaseAndSelfMetas(getMetadata(e))) {\n let list = entitiesByType.get(m);\n if (!list) {\n list = [];\n entitiesByType.set(m, list);\n }\n list.push(e);\n }\n }\n return [...entitiesByType.entries()];\n}\n\ntype BindingColumn = OpColumn & Pick<FieldColumn, \"dbValue\">;\ntype EntityWithData = Entity & { __data: { data: Record<string, unknown> } };\n\n/**\n * Builds a *columnar* array of bindings for the given `entities` and `columns`.\n *\n * We use a columnar approach, instead of `rows[]`, to best match the unnest-based\n * column operations in our `batchInsert` / `batchUpdate` SQL statements.\n */\nfunction collectBindings(\n entities: Entity[],\n tableName: string,\n columns: BindingColumn[],\n fixups: InsertFixup[] | undefined,\n): any[][] {\n const entityCount = entities.length;\n const bindings: any[][] = new Array(columns.length);\n const entitiesWithData = entities as EntityWithData[];\n // Cache data separately; this benchmarks faster than `entities[i].__data.data` in the inner loop.\n const entityData = new Array(entityCount);\n for (let i = 0; i < entityCount; i++) entityData[i] = entitiesWithData[i].__data.data;\n for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n const column = columns[columnIndex];\n const columnValues: any[] = new Array(entityCount);\n for (let entityIndex = 0; entityIndex < entityCount; entityIndex++) {\n columnValues[entityIndex] =\n column.dbValue(entityData[entityIndex], entities[entityIndex], tableName, fixups) ?? null;\n }\n bindings[columnIndex] = columnValues;\n }\n return bindings;\n}\n/**\n * Given `fixups` that indicate where we inserted `NULL` into non-deferred FKs, create `UPDATE`s\n * that insert the value now that the FK check will succeed.\n */\nfunction translateFixupsIntoUpdates(ops: Ops, fixups: InsertFixup[]): void {\n // Create a single `UPDATE` for the N fixups we might have for each table/column combination\n groupBy(fixups, (f) => `${f.tableName}.${f.column.columnName}`).forEach((fixups) => {\n const { tableName, entity, column } = fixups[0];\n ops.updates.push({\n tableName,\n columns: [getMetadata(entity).fields[\"id\"].serde!.columns[0], column],\n columnValues: [\n // Make 1 column of ids, and 1 column of values\n fixups.map((fixup) => fixup.entity.id),\n fixups.map((fixup) => fixup.value),\n ],\n updatedAt: undefined,\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EntityWriter.cjs","names":["groupBy","getMetadata","hasSerde","getInstanceData","isChangeableField","getBaseSelfAndSubMetas","keyToNumber","getBaseAndSelfMetas"],"sources":["../../src/drivers/EntityWriter.ts"],"sourcesContent":["import { getInstanceData } from \"src/BaseEntity.ts\";\nimport { type Entity } from \"src/Entity.ts\";\nimport {\n type EntityMetadata,\n type Field,\n type PrimitiveField,\n getBaseAndSelfMetas,\n getBaseSelfAndSubMetas,\n getMetadata,\n} from \"src/EntityMetadata.ts\";\nimport { getField, isChangeableField } from \"src/fields.ts\";\nimport type { Todo } from \"src/flush/Todo.ts\";\nimport { keyToNumber } from \"src/keys.ts\";\nimport { type FieldColumn, type TimestampSerde, hasSerde } from \"src/serde/fieldSerde.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\n/** A simplified view of columns, with only the keys necessary to create SQL statements. */\nexport type OpColumn = {\n columnName: string;\n dbType: string;\n isNullableArray?: boolean;\n /**\n * The unnest null/set marker as the physical db type, removed before storage.\n *\n * I.e. strings use \"\", integers use 1, and Temporal dates use \"2000-01-01\".\n * The marker must have the same physical db type as real array elements, even for empty arrays.\n *\n * Nullable arrays prepend null for SQL NULL or this marker for a present array, then pad shorter\n * arrays with nulls to make the batch rectangular. With nullable=true, unnest_arrays checks the\n * first element: null means SQL NULL; otherwise it discards that marker and removes null padding.\n * All remaining non-null elements are real values, so null elements within stored arrays are not supported.\n */\n readonly notNullMarker?: unknown;\n};\nexport type InsertOp = { tableName: string; columns: OpColumn[]; columnValues: any[][] };\n/**\n * A logical `update` operation.\n *\n * If we're using op locks, `columns` will include both the new `updatedAt` value (as bumped\n * by the `EntityManager` on mutate), as well as an extra/last \"original updated at\" column\n * (with respective pre-update values in the `rows` data), for including in the conditional\n * update clause.\n */\nexport type UpdateOp = {\n tableName: string;\n columns: OpColumn[];\n columnValues: any[][];\n updatedAt: string | undefined;\n};\nexport type DeleteOp = { tableName: string; ids: any[] };\n\ntype Ops = { inserts: InsertOp[]; updates: UpdateOp[]; deletes: DeleteOp[] };\n\n/**\n * In schemas with entity FK cycles, we may insert a dummy/null value, and later fix it up\n * before the transaction commits.\n *\n * I.e. `INSERT author.favorite_book_id` as `NULL` and then later do an `UPDATE author\n * SET favorite_book_id = 1` after the book has been inserted/we have the id.\n */\nexport type InsertFixup = {\n entity: Entity;\n tableName: string;\n column: OpColumn;\n value: any;\n};\n\n/**\n * Builds AST-ish `Ops` for the inserts/updates/deletes in `todos`.\n *\n * This helps decouple the database-specific driver/library from the knowledge\n * of turning entities into table operations, i.e. so they can be unaware of\n * complexities like field -> column mapping, oplocks, and class table inheritance.\n *\n * We still assume Postgres b/c we assume all ids have been pre-assigned for INSERTs,\n * and don't do any topological sorting of cross-row FK dependencies.\n *\n * But this should let us experiment with knex vs. raw client/etc.\n */\nexport function generateOps(todos: Record<string, Todo>): Ops {\n const ops: Ops = { inserts: [], updates: [], deletes: [] };\n const fixups: InsertFixup[] = [];\n // Would be nice to conditionally sort\n const sorted = Object.values(todos).sort(\n (a, b) => (a.metadata.nonDeferredFkOrder ?? 0) - (b.metadata.nonDeferredFkOrder ?? 0),\n );\n for (const todo of sorted) {\n addInserts(ops, todo, fixups);\n addUpdates(ops, todo);\n addDeletes(ops, todo);\n }\n if (fixups.length > 0) translateFixupsIntoUpdates(ops, fixups);\n return ops;\n}\n\nfunction addInserts(ops: Ops, todo: Todo, fixups: InsertFixup[]): void {\n if (todo.inserts.length > 0) {\n // If we have subtypes, this todo.metadata will always be the base type\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n // Insert into each of the CTI tables\n for (const [meta, group] of groupEntitiesByTable(todo.inserts)) {\n ops.inserts.push(newInsertOp(meta, group, fixups));\n }\n } else if (meta.inheritanceType === \"sti\") {\n // One INSERT per subtype, so each statement's column list spans exactly what that subtype can\n // set. A single statement across subtypes has to bind NULL for the other subtypes' columns,\n // which defeats their database defaults, and outright fails for notNull ones.\n for (const group of groupBy(todo.inserts, (e) => getMetadata(e)).values()) {\n ops.inserts.push(newStiInsertOp(meta, group, fixups));\n }\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n ops.inserts.push(newInsertOp(meta, todo.inserts, fixups));\n }\n }\n}\n\nfunction newInsertOp(meta: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // Purposefully use `meta.fields` instead of `allFields` b/c each CTI table has its own `InsertOp`\n const columns = Object.values(meta.fields)\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, meta.tableName, columns, fixups);\n return { tableName: meta.tableName, columns, columnValues };\n}\n\nfunction newStiInsertOp(root: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // `allFields` is the base's fields plus this subtype's own, i.e. everything this subtype can write.\n // Anything it can't write is left out of the statement entirely, so the database applies its default.\n const fields: Field[] = Object.values(getMetadata(entities[0]).allFields);\n const columns = fields.filter(hasSerde).flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, root.tableName, columns, fixups);\n return { tableName: root.tableName, columns, columnValues };\n}\n\nfunction addUpdates(ops: Ops, todo: Todo): void {\n if (todo.updates.length > 0) {\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n for (const [meta, group] of groupEntitiesByTable(todo.updates)) {\n const op = newUpdateOp(meta, group);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, group);\n }\n } else if (meta.inheritanceType === \"sti\") {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n }\n }\n}\n\n/**\n * Writes changed `lazy` columns via a targeted `UPDATE table SET lazy_col = ... WHERE id in (...)`.\n *\n * Lazy columns are excluded from the main batch UPDATE (which spans the batch's union of changed fields),\n * so that entities in the batch that never loaded a lazy column keep their existing db value instead of\n * being overwritten with null. Here we write each lazy column only for the entities that actually changed it.\n */\nfunction addLazyUpdates(ops: Ops, meta: EntityMetadata, entities: Entity[]): void {\n const idColumn = meta.fields[\"id\"].serde!.columns[0];\n for (const fieldName of meta.lazyFieldNames!) {\n const field = meta.fields[fieldName];\n if (!hasSerde(field)) continue;\n const changed = entities.filter((e) => fieldName in getInstanceData(e).changedData);\n if (changed.length === 0) continue;\n const columns: BindingColumn[] = [idColumn, ...field.serde.columns];\n const columnValues = collectBindings(changed, meta.tableName, columns, undefined);\n ops.updates.push({ tableName: meta.tableName, columns, columnValues, updatedAt: undefined });\n }\n}\n\nfunction newUpdateOp(meta: EntityMetadata, entities: Entity[]): UpdateOp | undefined {\n // We only include changed fields in our `UPDATE`--maybe we could change this\n // to always use the same fields, to take advantage of Prepared Statements.\n const changedFields = new Set<string>();\n for (const entity of entities) {\n for (const fieldName in getInstanceData(entity).changedData) changedFields.add(fieldName);\n }\n // Sometimes with derived fields, an instance will be marked as an update, but if the derived field hasn't\n // actually changed, it'll be a noop, so just short-circuit if it looks like that happened. Unless touched.\n if (changedFields.size === 0 && !entities.some((e) => getInstanceData(e).isTouched)) {\n return undefined;\n }\n\n // We may have loaded a1 and a2, and changed a1.firstName, and a2.lastName, but either one\n // might be missing the other's changed fields in it's lazy-initialized data field...\n // (Use `?` because subtypes won't have the updatedAt, and it will be handled by the base type\n // `newUpdateUp`--except STI where we're doing it all in one go, probably needs checked here.)\n const updatedAt = meta.timestampFields?.updatedAt;\n changedFields.add(\"id\");\n if (updatedAt) changedFields.add(updatedAt);\n // `lazy` columns are deliberately excluded from the batch UPDATE and written by `addLazyUpdates`; an\n // entity that never loaded a lazy column isn't in `row`, so forcing `getField` here would clobber it to null.\n const lazyFieldNames = meta.lazyFieldNames!;\n for (const entity of entities) {\n const { data } = getInstanceData(entity);\n for (const key of changedFields) {\n // Check isChangeableField because we might be updating the base `publishers` table\n // and `originalData` might have fields from a subclass `large_publishers` table.\n if (!(key in data) && isChangeableField(entity, key) && !lazyFieldNames.has(key)) {\n getField(entity, key, true);\n }\n }\n }\n\n const columns: Array<BindingColumn> = (\n meta.inheritanceType === \"sti\"\n ? // Hack this one handling of STI into here...\n getBaseSelfAndSubMetas(meta).flatMap((meta) =>\n meta.stiDiscriminatorField\n ? Object.values(meta.fields)\n : Object.values(meta.fields).filter((f) => f.fieldName !== \"id\"),\n )\n : Object.values(meta.fields)\n )\n .filter((f) => changedFields.has(f.fieldName))\n .filter((f) => !lazyFieldNames.has(f.fieldName))\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n\n // If we're using class table inheritance, base/child tables may not have any columns to update\n if (columns.length === 1) {\n return undefined;\n }\n\n // We already have the bumped updated_at column, but also include the original updated_at for the data CTE\n if (updatedAt) {\n const serde = meta.fields[updatedAt].serde as TimestampSerde<unknown>;\n columns.push({\n columnName: \"__original_updated_at\",\n dbType: \"timestamptz\",\n dbValue: (_, entity) => serde.dbValue(getInstanceData(entity).originalData),\n });\n }\n\n const columnValues = collectBindings(entities, meta.tableName, columns, []);\n const updatedAtColumn = updatedAt ? (meta.fields[updatedAt] as PrimitiveField).serde.columns[0] : undefined;\n return { tableName: meta.tableName, columns, updatedAt: updatedAtColumn?.columnName, columnValues };\n}\n\nfunction addDeletes(ops: Ops, todo: Todo): void {\n if (todo.deletes.length > 0) {\n const meta = todo.metadata;\n const ids = todo.deletes.map((e) => keyToNumber(meta, e.idTagged!).toString());\n if (meta.subTypes.length > 0) {\n getBaseSelfAndSubMetas(meta).forEach((meta) => {\n ops.deletes.push({ tableName: meta.tableName, ids });\n });\n } else {\n ops.deletes.push({ tableName: meta.tableName, ids });\n }\n }\n}\n\nfunction groupEntitiesByTable(entities: Entity[]): Array<[EntityMetadata, Entity[]]> {\n const entitiesByType: Map<EntityMetadata, Entity[]> = new Map();\n for (const e of entities) {\n for (const m of getBaseAndSelfMetas(getMetadata(e))) {\n let list = entitiesByType.get(m);\n if (!list) {\n list = [];\n entitiesByType.set(m, list);\n }\n list.push(e);\n }\n }\n return [...entitiesByType.entries()];\n}\n\ntype BindingColumn = OpColumn & Pick<FieldColumn, \"dbValue\">;\ntype EntityWithData = Entity & { __data: { data: Record<string, unknown> } };\n\n/**\n * Builds a *columnar* array of bindings for the given `entities` and `columns`.\n *\n * We use a columnar approach, instead of `rows[]`, to best match the unnest-based\n * column operations in our `batchInsert` / `batchUpdate` SQL statements.\n */\nfunction collectBindings(\n entities: Entity[],\n tableName: string,\n columns: BindingColumn[],\n fixups: InsertFixup[] | undefined,\n): any[][] {\n const entityCount = entities.length;\n const bindings: any[][] = new Array(columns.length);\n const entitiesWithData = entities as EntityWithData[];\n // Cache data separately; this benchmarks faster than `entities[i].__data.data` in the inner loop.\n const entityData = new Array(entityCount);\n for (let i = 0; i < entityCount; i++) entityData[i] = entitiesWithData[i].__data.data;\n for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n const column = columns[columnIndex];\n const columnValues: any[] = new Array(entityCount);\n for (let entityIndex = 0; entityIndex < entityCount; entityIndex++) {\n columnValues[entityIndex] =\n column.dbValue(entityData[entityIndex], entities[entityIndex], tableName, fixups) ?? null;\n }\n bindings[columnIndex] = columnValues;\n }\n return bindings;\n}\n/**\n * Given `fixups` that indicate where we inserted `NULL` into non-deferred FKs, create `UPDATE`s\n * that insert the value now that the FK check will succeed.\n */\nfunction translateFixupsIntoUpdates(ops: Ops, fixups: InsertFixup[]): void {\n // Create a single `UPDATE` for the N fixups we might have for each table/column combination\n groupBy(fixups, (f) => `${f.tableName}.${f.column.columnName}`).forEach((fixups) => {\n const { tableName, entity, column } = fixups[0];\n ops.updates.push({\n tableName,\n columns: [getMetadata(entity).fields[\"id\"].serde!.columns[0], column],\n columnValues: [\n // Make 1 column of ids, and 1 column of values\n fixups.map((fixup) => fixup.entity.id),\n fixups.map((fixup) => fixup.value),\n ],\n updatedAt: undefined,\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -6,6 +6,18 @@ export type OpColumn = {
|
|
|
6
6
|
columnName: string;
|
|
7
7
|
dbType: string;
|
|
8
8
|
isNullableArray?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The unnest null/set marker as the physical db type, removed before storage.
|
|
11
|
+
*
|
|
12
|
+
* I.e. strings use "", integers use 1, and Temporal dates use "2000-01-01".
|
|
13
|
+
* The marker must have the same physical db type as real array elements, even for empty arrays.
|
|
14
|
+
*
|
|
15
|
+
* Nullable arrays prepend null for SQL NULL or this marker for a present array, then pad shorter
|
|
16
|
+
* arrays with nulls to make the batch rectangular. With nullable=true, unnest_arrays checks the
|
|
17
|
+
* first element: null means SQL NULL; otherwise it discards that marker and removes null padding.
|
|
18
|
+
* All remaining non-null elements are real values, so null elements within stored arrays are not supported.
|
|
19
|
+
*/
|
|
20
|
+
readonly notNullMarker?: unknown;
|
|
9
21
|
};
|
|
10
22
|
export type InsertOp = {
|
|
11
23
|
tableName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityWriter.d.cts","names":[],"sources":["../../src/drivers/EntityWriter.ts"],"mappings":";;;;YAiBY;
|
|
1
|
+
{"version":3,"file":"EntityWriter.d.cts","names":[],"sources":["../../src/drivers/EntityWriter.ts"],"mappings":";;;;YAiBY;EACV;EACA;EACA;;;;;;;;;;;;WAYS;;YAEC;EAAa;EAAmB,SAAS;EAAY;;;;;;;;;;YASrD;EACV;EACA,SAAS;EACT;EACA;;YAEU;EAAa;EAAmB;;KAEvC;EAAQ,SAAS;EAAY,SAAS;EAAY,SAAS;;;;;;;;;YASpD;EACV,QAAQ;EACR;EACA,QAAQ;EACR;;;;;;;;;;;;;;wBAec,YAAY,OAAO,eAAe,QAAQ"}
|
|
@@ -6,6 +6,18 @@ export type OpColumn = {
|
|
|
6
6
|
columnName: string;
|
|
7
7
|
dbType: string;
|
|
8
8
|
isNullableArray?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The unnest null/set marker as the physical db type, removed before storage.
|
|
11
|
+
*
|
|
12
|
+
* I.e. strings use "", integers use 1, and Temporal dates use "2000-01-01".
|
|
13
|
+
* The marker must have the same physical db type as real array elements, even for empty arrays.
|
|
14
|
+
*
|
|
15
|
+
* Nullable arrays prepend null for SQL NULL or this marker for a present array, then pad shorter
|
|
16
|
+
* arrays with nulls to make the batch rectangular. With nullable=true, unnest_arrays checks the
|
|
17
|
+
* first element: null means SQL NULL; otherwise it discards that marker and removes null padding.
|
|
18
|
+
* All remaining non-null elements are real values, so null elements within stored arrays are not supported.
|
|
19
|
+
*/
|
|
20
|
+
readonly notNullMarker?: unknown;
|
|
9
21
|
};
|
|
10
22
|
export type InsertOp = {
|
|
11
23
|
tableName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityWriter.d.mts","names":[],"sources":["../../src/drivers/EntityWriter.ts"],"mappings":";;;;YAiBY;
|
|
1
|
+
{"version":3,"file":"EntityWriter.d.mts","names":[],"sources":["../../src/drivers/EntityWriter.ts"],"mappings":";;;;YAiBY;EACV;EACA;EACA;;;;;;;;;;;;WAYS;;YAEC;EAAa;EAAmB,SAAS;EAAY;;;;;;;;;;YASrD;EACV;EACA,SAAS;EACT;EACA;;YAEU;EAAa;EAAmB;;KAEvC;EAAQ,SAAS;EAAY,SAAS;EAAY,SAAS;;;;;;;;;YASpD;EACV,QAAQ;EACR;EACA,QAAQ;EACR;;;;;;;;;;;;;;wBAec,YAAY,OAAO,eAAe,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityWriter.js","names":[],"sources":["../../src/drivers/EntityWriter.ts"],"sourcesContent":["import { getInstanceData } from \"src/BaseEntity.ts\";\nimport { type Entity } from \"src/Entity.ts\";\nimport {\n type EntityMetadata,\n type Field,\n type PrimitiveField,\n getBaseAndSelfMetas,\n getBaseSelfAndSubMetas,\n getMetadata,\n} from \"src/EntityMetadata.ts\";\nimport { getField, isChangeableField } from \"src/fields.ts\";\nimport type { Todo } from \"src/flush/Todo.ts\";\nimport { keyToNumber } from \"src/keys.ts\";\nimport { type FieldColumn, type TimestampSerde, hasSerde } from \"src/serde/fieldSerde.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\n/** A simplified view of columns, with only the keys necessary to create SQL statements. */\nexport type OpColumn = { columnName: string; dbType: string; isNullableArray?: boolean };\nexport type InsertOp = { tableName: string; columns: OpColumn[]; columnValues: any[][] };\n/**\n * A logical `update` operation.\n *\n * If we're using op locks, `columns` will include both the new `updatedAt` value (as bumped\n * by the `EntityManager` on mutate), as well as an extra/last \"original updated at\" column\n * (with respective pre-update values in the `rows` data), for including in the conditional\n * update clause.\n */\nexport type UpdateOp = {\n tableName: string;\n columns: OpColumn[];\n columnValues: any[][];\n updatedAt: string | undefined;\n};\nexport type DeleteOp = { tableName: string; ids: any[] };\n\ntype Ops = { inserts: InsertOp[]; updates: UpdateOp[]; deletes: DeleteOp[] };\n\n/**\n * In schemas with entity FK cycles, we may insert a dummy/null value, and later fix it up\n * before the transaction commits.\n *\n * I.e. `INSERT author.favorite_book_id` as `NULL` and then later do an `UPDATE author\n * SET favorite_book_id = 1` after the book has been inserted/we have the id.\n */\nexport type InsertFixup = {\n entity: Entity;\n tableName: string;\n column: OpColumn;\n value: any;\n};\n\n/**\n * Builds AST-ish `Ops` for the inserts/updates/deletes in `todos`.\n *\n * This helps decouple the database-specific driver/library from the knowledge\n * of turning entities into table operations, i.e. so they can be unaware of\n * complexities like field -> column mapping, oplocks, and class table inheritance.\n *\n * We still assume Postgres b/c we assume all ids have been pre-assigned for INSERTs,\n * and don't do any topological sorting of cross-row FK dependencies.\n *\n * But this should let us experiment with knex vs. raw client/etc.\n */\nexport function generateOps(todos: Record<string, Todo>): Ops {\n const ops: Ops = { inserts: [], updates: [], deletes: [] };\n const fixups: InsertFixup[] = [];\n // Would be nice to conditionally sort\n const sorted = Object.values(todos).sort(\n (a, b) => (a.metadata.nonDeferredFkOrder ?? 0) - (b.metadata.nonDeferredFkOrder ?? 0),\n );\n for (const todo of sorted) {\n addInserts(ops, todo, fixups);\n addUpdates(ops, todo);\n addDeletes(ops, todo);\n }\n if (fixups.length > 0) translateFixupsIntoUpdates(ops, fixups);\n return ops;\n}\n\nfunction addInserts(ops: Ops, todo: Todo, fixups: InsertFixup[]): void {\n if (todo.inserts.length > 0) {\n // If we have subtypes, this todo.metadata will always be the base type\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n // Insert into each of the CTI tables\n for (const [meta, group] of groupEntitiesByTable(todo.inserts)) {\n ops.inserts.push(newInsertOp(meta, group, fixups));\n }\n } else if (meta.inheritanceType === \"sti\") {\n // One INSERT per subtype, so each statement's column list spans exactly what that subtype can\n // set. A single statement across subtypes has to bind NULL for the other subtypes' columns,\n // which defeats their database defaults, and outright fails for notNull ones.\n for (const group of groupBy(todo.inserts, (e) => getMetadata(e)).values()) {\n ops.inserts.push(newStiInsertOp(meta, group, fixups));\n }\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n ops.inserts.push(newInsertOp(meta, todo.inserts, fixups));\n }\n }\n}\n\nfunction newInsertOp(meta: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // Purposefully use `meta.fields` instead of `allFields` b/c each CTI table has its own `InsertOp`\n const columns = Object.values(meta.fields)\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, meta.tableName, columns, fixups);\n return { tableName: meta.tableName, columns, columnValues };\n}\n\nfunction newStiInsertOp(root: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // `allFields` is the base's fields plus this subtype's own, i.e. everything this subtype can write.\n // Anything it can't write is left out of the statement entirely, so the database applies its default.\n const fields: Field[] = Object.values(getMetadata(entities[0]).allFields);\n const columns = fields.filter(hasSerde).flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, root.tableName, columns, fixups);\n return { tableName: root.tableName, columns, columnValues };\n}\n\nfunction addUpdates(ops: Ops, todo: Todo): void {\n if (todo.updates.length > 0) {\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n for (const [meta, group] of groupEntitiesByTable(todo.updates)) {\n const op = newUpdateOp(meta, group);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, group);\n }\n } else if (meta.inheritanceType === \"sti\") {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n }\n }\n}\n\n/**\n * Writes changed `lazy` columns via a targeted `UPDATE table SET lazy_col = ... WHERE id in (...)`.\n *\n * Lazy columns are excluded from the main batch UPDATE (which spans the batch's union of changed fields),\n * so that entities in the batch that never loaded a lazy column keep their existing db value instead of\n * being overwritten with null. Here we write each lazy column only for the entities that actually changed it.\n */\nfunction addLazyUpdates(ops: Ops, meta: EntityMetadata, entities: Entity[]): void {\n const idColumn = meta.fields[\"id\"].serde!.columns[0];\n for (const fieldName of meta.lazyFieldNames!) {\n const field = meta.fields[fieldName];\n if (!hasSerde(field)) continue;\n const changed = entities.filter((e) => fieldName in getInstanceData(e).changedData);\n if (changed.length === 0) continue;\n const columns: BindingColumn[] = [idColumn, ...field.serde.columns];\n const columnValues = collectBindings(changed, meta.tableName, columns, undefined);\n ops.updates.push({ tableName: meta.tableName, columns, columnValues, updatedAt: undefined });\n }\n}\n\nfunction newUpdateOp(meta: EntityMetadata, entities: Entity[]): UpdateOp | undefined {\n // We only include changed fields in our `UPDATE`--maybe we could change this\n // to always use the same fields, to take advantage of Prepared Statements.\n const changedFields = new Set<string>();\n for (const entity of entities) {\n for (const fieldName in getInstanceData(entity).changedData) changedFields.add(fieldName);\n }\n // Sometimes with derived fields, an instance will be marked as an update, but if the derived field hasn't\n // actually changed, it'll be a noop, so just short-circuit if it looks like that happened. Unless touched.\n if (changedFields.size === 0 && !entities.some((e) => getInstanceData(e).isTouched)) {\n return undefined;\n }\n\n // We may have loaded a1 and a2, and changed a1.firstName, and a2.lastName, but either one\n // might be missing the other's changed fields in it's lazy-initialized data field...\n // (Use `?` because subtypes won't have the updatedAt, and it will be handled by the base type\n // `newUpdateUp`--except STI where we're doing it all in one go, probably needs checked here.)\n const updatedAt = meta.timestampFields?.updatedAt;\n changedFields.add(\"id\");\n if (updatedAt) changedFields.add(updatedAt);\n // `lazy` columns are deliberately excluded from the batch UPDATE and written by `addLazyUpdates`; an\n // entity that never loaded a lazy column isn't in `row`, so forcing `getField` here would clobber it to null.\n const lazyFieldNames = meta.lazyFieldNames!;\n for (const entity of entities) {\n const { data } = getInstanceData(entity);\n for (const key of changedFields) {\n // Check isChangeableField because we might be updating the base `publishers` table\n // and `originalData` might have fields from a subclass `large_publishers` table.\n if (!(key in data) && isChangeableField(entity, key) && !lazyFieldNames.has(key)) {\n getField(entity, key, true);\n }\n }\n }\n\n const columns: Array<BindingColumn> = (\n meta.inheritanceType === \"sti\"\n ? // Hack this one handling of STI into here...\n getBaseSelfAndSubMetas(meta).flatMap((meta) =>\n meta.stiDiscriminatorField\n ? Object.values(meta.fields)\n : Object.values(meta.fields).filter((f) => f.fieldName !== \"id\"),\n )\n : Object.values(meta.fields)\n )\n .filter((f) => changedFields.has(f.fieldName))\n .filter((f) => !lazyFieldNames.has(f.fieldName))\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n\n // If we're using class table inheritance, base/child tables may not have any columns to update\n if (columns.length === 1) {\n return undefined;\n }\n\n // We already have the bumped updated_at column, but also include the original updated_at for the data CTE\n if (updatedAt) {\n const serde = meta.fields[updatedAt].serde as TimestampSerde<unknown>;\n columns.push({\n columnName: \"__original_updated_at\",\n dbType: \"timestamptz\",\n dbValue: (_, entity) => serde.dbValue(getInstanceData(entity).originalData),\n });\n }\n\n const columnValues = collectBindings(entities, meta.tableName, columns, []);\n const updatedAtColumn = updatedAt ? (meta.fields[updatedAt] as PrimitiveField).serde.columns[0] : undefined;\n return { tableName: meta.tableName, columns, updatedAt: updatedAtColumn?.columnName, columnValues };\n}\n\nfunction addDeletes(ops: Ops, todo: Todo): void {\n if (todo.deletes.length > 0) {\n const meta = todo.metadata;\n const ids = todo.deletes.map((e) => keyToNumber(meta, e.idTagged!).toString());\n if (meta.subTypes.length > 0) {\n getBaseSelfAndSubMetas(meta).forEach((meta) => {\n ops.deletes.push({ tableName: meta.tableName, ids });\n });\n } else {\n ops.deletes.push({ tableName: meta.tableName, ids });\n }\n }\n}\n\nfunction groupEntitiesByTable(entities: Entity[]): Array<[EntityMetadata, Entity[]]> {\n const entitiesByType: Map<EntityMetadata, Entity[]> = new Map();\n for (const e of entities) {\n for (const m of getBaseAndSelfMetas(getMetadata(e))) {\n let list = entitiesByType.get(m);\n if (!list) {\n list = [];\n entitiesByType.set(m, list);\n }\n list.push(e);\n }\n }\n return [...entitiesByType.entries()];\n}\n\ntype BindingColumn = OpColumn & Pick<FieldColumn, \"dbValue\">;\ntype EntityWithData = Entity & { __data: { data: Record<string, unknown> } };\n\n/**\n * Builds a *columnar* array of bindings for the given `entities` and `columns`.\n *\n * We use a columnar approach, instead of `rows[]`, to best match the unnest-based\n * column operations in our `batchInsert` / `batchUpdate` SQL statements.\n */\nfunction collectBindings(\n entities: Entity[],\n tableName: string,\n columns: BindingColumn[],\n fixups: InsertFixup[] | undefined,\n): any[][] {\n const entityCount = entities.length;\n const bindings: any[][] = new Array(columns.length);\n const entitiesWithData = entities as EntityWithData[];\n // Cache data separately; this benchmarks faster than `entities[i].__data.data` in the inner loop.\n const entityData = new Array(entityCount);\n for (let i = 0; i < entityCount; i++) entityData[i] = entitiesWithData[i].__data.data;\n for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n const column = columns[columnIndex];\n const columnValues: any[] = new Array(entityCount);\n for (let entityIndex = 0; entityIndex < entityCount; entityIndex++) {\n columnValues[entityIndex] =\n column.dbValue(entityData[entityIndex], entities[entityIndex], tableName, fixups) ?? null;\n }\n bindings[columnIndex] = columnValues;\n }\n return bindings;\n}\n/**\n * Given `fixups` that indicate where we inserted `NULL` into non-deferred FKs, create `UPDATE`s\n * that insert the value now that the FK check will succeed.\n */\nfunction translateFixupsIntoUpdates(ops: Ops, fixups: InsertFixup[]): void {\n // Create a single `UPDATE` for the N fixups we might have for each table/column combination\n groupBy(fixups, (f) => `${f.tableName}.${f.column.columnName}`).forEach((fixups) => {\n const { tableName, entity, column } = fixups[0];\n ops.updates.push({\n tableName,\n columns: [getMetadata(entity).fields[\"id\"].serde!.columns[0], column],\n columnValues: [\n // Make 1 column of ids, and 1 column of values\n fixups.map((fixup) => fixup.entity.id),\n fixups.map((fixup) => fixup.value),\n ],\n updatedAt: undefined,\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,YAAY,OAAkC;CAC5D,MAAM,MAAW;EAAE,SAAS,CAAC;EAAG,SAAS,CAAC;EAAG,SAAS,CAAC;CAAE;CACzD,MAAM,SAAwB,CAAC;CAE/B,MAAM,SAAS,OAAO,OAAO,KAAK,CAAC,CAAC,MACjC,GAAG,OAAO,EAAE,SAAS,sBAAsB,MAAM,EAAE,SAAS,sBAAsB,EACrF;CACA,KAAK,MAAM,QAAQ,QAAQ;EACzB,WAAW,KAAK,MAAM,MAAM;EAC5B,WAAW,KAAK,IAAI;EACpB,WAAW,KAAK,IAAI;CACtB;CACA,IAAI,OAAO,SAAS,GAAG,2BAA2B,KAAK,MAAM;CAC7D,OAAO;AACT;AAEA,SAAS,WAAW,KAAU,MAAY,QAA6B;CACrE,IAAI,KAAK,QAAQ,SAAS,GAAG;EAE3B,MAAM,OAAO,KAAK;EAClB,IAAI,KAAK,SAAS,SAAS,GAAG;GAC5B,IAAI,KAAK,oBAAoB,OAE3B,KAAK,MAAM,CAAC,MAAM,UAAU,qBAAqB,KAAK,OAAO,GAC3D,IAAI,QAAQ,KAAK,YAAY,MAAM,OAAO,MAAM,CAAC;QAE9C,IAAI,KAAK,oBAAoB,OAIlC,KAAK,MAAM,SAAS,QAAQ,KAAK,UAAU,MAAM,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,GACtE,IAAI,QAAQ,KAAK,eAAe,MAAM,OAAO,MAAM,CAAC;QAGtD,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,4CAA4C,KAAK,iBAAiB;EAE9G,OACE,IAAI,QAAQ,KAAK,YAAY,MAAM,KAAK,SAAS,MAAM,CAAC;CAE5D;AACF;AAEA,SAAS,YAAY,MAAsB,UAAoB,QAAiC;CAE9F,MAAM,UAAU,OAAO,OAAO,KAAK,MAAM,CAAC,CACvC,OAAO,QAAQ,CAAC,CAChB,SAAS,MAAM,EAAE,MAAM,OAAO;CACjC,MAAM,eAAe,gBAAgB,UAAU,KAAK,WAAW,SAAS,MAAM;CAC9E,OAAO;EAAE,WAAW,KAAK;EAAW;EAAS;CAAa;AAC5D;AAEA,SAAS,eAAe,MAAsB,UAAoB,QAAiC;CAIjG,MAAM,UADkB,OAAO,OAAO,YAAY,SAAS,EAAE,CAAC,CAAC,SAC1C,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,OAAO;CACtE,MAAM,eAAe,gBAAgB,UAAU,KAAK,WAAW,SAAS,MAAM;CAC9E,OAAO;EAAE,WAAW,KAAK;EAAW;EAAS;CAAa;AAC5D;AAEA,SAAS,WAAW,KAAU,MAAkB;CAC9C,IAAI,KAAK,QAAQ,SAAS,GAAG;EAC3B,MAAM,OAAO,KAAK;EAClB,IAAI,KAAK,SAAS,SAAS,GAAG;GAC5B,IAAI,KAAK,oBAAoB,OAC3B,KAAK,MAAM,CAAC,MAAM,UAAU,qBAAqB,KAAK,OAAO,GAAG;IAC9D,MAAM,KAAK,YAAY,MAAM,KAAK;IAClC,IAAI,IAAI,IAAI,QAAQ,KAAK,EAAE;IAC3B,eAAe,KAAK,MAAM,KAAK;GACjC;QACK,IAAI,KAAK,oBAAoB,OAAO;IACzC,MAAM,KAAK,YAAY,MAAM,KAAK,OAAO;IACzC,IAAI,IAAI,IAAI,QAAQ,KAAK,EAAE;IAC3B,eAAe,KAAK,MAAM,KAAK,OAAO;GACxC,OACE,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,4CAA4C,KAAK,iBAAiB;EAE9G,OAAO;GACL,MAAM,KAAK,YAAY,MAAM,KAAK,OAAO;GACzC,IAAI,IAAI,IAAI,QAAQ,KAAK,EAAE;GAC3B,eAAe,KAAK,MAAM,KAAK,OAAO;EACxC;CACF;AACF;;;;;;;;AASA,SAAS,eAAe,KAAU,MAAsB,UAA0B;CAChF,MAAM,WAAW,KAAK,OAAO,KAAK,CAAC,MAAO,QAAQ;CAClD,KAAK,MAAM,aAAa,KAAK,gBAAiB;EAC5C,MAAM,QAAQ,KAAK,OAAO;EAC1B,IAAI,CAAC,SAAS,KAAK,GAAG;EACtB,MAAM,UAAU,SAAS,QAAQ,MAAM,aAAa,gBAAgB,CAAC,CAAC,CAAC,WAAW;EAClF,IAAI,QAAQ,WAAW,GAAG;EAC1B,MAAM,UAA2B,CAAC,UAAU,GAAG,MAAM,MAAM,OAAO;EAClE,MAAM,eAAe,gBAAgB,SAAS,KAAK,WAAW,SAAS,KAAA,CAAS;EAChF,IAAI,QAAQ,KAAK;GAAE,WAAW,KAAK;GAAW;GAAS;GAAc,WAAW,KAAA;EAAU,CAAC;CAC7F;AACF;AAEA,SAAS,YAAY,MAAsB,UAA0C;CAGnF,MAAM,gCAAgB,IAAI,IAAY;CACtC,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,aAAa,gBAAgB,MAAM,CAAC,CAAC,aAAa,cAAc,IAAI,SAAS;CAI1F,IAAI,cAAc,SAAS,KAAK,CAAC,SAAS,MAAM,MAAM,gBAAgB,CAAC,CAAC,CAAC,SAAS,GAChF;CAOF,MAAM,YAAY,KAAK,iBAAiB;CACxC,cAAc,IAAI,IAAI;CACtB,IAAI,WAAW,cAAc,IAAI,SAAS;CAG1C,MAAM,iBAAiB,KAAK;CAC5B,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,EAAE,SAAS,gBAAgB,MAAM;EACvC,KAAK,MAAM,OAAO,eAGhB,IAAI,EAAE,OAAO,SAAS,kBAAkB,QAAQ,GAAG,KAAK,CAAC,eAAe,IAAI,GAAG,GAC7E,SAAS,QAAQ,KAAK,IAAI;CAGhC;CAEA,MAAM,WACJ,KAAK,oBAAoB,QAErB,uBAAuB,IAAI,CAAC,CAAC,SAAS,SACpC,KAAK,wBACD,OAAO,OAAO,KAAK,MAAM,IACzB,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,QAAQ,MAAM,EAAE,cAAc,IAAI,CACnE,IACA,OAAO,OAAO,KAAK,MAAM,EAAA,CAE5B,QAAQ,MAAM,cAAc,IAAI,EAAE,SAAS,CAAC,CAAC,CAC7C,QAAQ,MAAM,CAAC,eAAe,IAAI,EAAE,SAAS,CAAC,CAAC,CAC/C,OAAO,QAAQ,CAAC,CAChB,SAAS,MAAM,EAAE,MAAM,OAAO;CAGjC,IAAI,QAAQ,WAAW,GACrB;CAIF,IAAI,WAAW;EACb,MAAM,QAAQ,KAAK,OAAO,UAAU,CAAC;EACrC,QAAQ,KAAK;GACX,YAAY;GACZ,QAAQ;GACR,UAAU,GAAG,WAAW,MAAM,QAAQ,gBAAgB,MAAM,CAAC,CAAC,YAAY;EAC5E,CAAC;CACH;CAEA,MAAM,eAAe,gBAAgB,UAAU,KAAK,WAAW,SAAS,CAAC,CAAC;CAC1E,MAAM,kBAAkB,YAAa,KAAK,OAAO,UAAU,CAAoB,MAAM,QAAQ,KAAK,KAAA;CAClG,OAAO;EAAE,WAAW,KAAK;EAAW;EAAS,WAAW,iBAAiB;EAAY;CAAa;AACpG;AAEA,SAAS,WAAW,KAAU,MAAkB;CAC9C,IAAI,KAAK,QAAQ,SAAS,GAAG;EAC3B,MAAM,OAAO,KAAK;EAClB,MAAM,MAAM,KAAK,QAAQ,KAAK,MAAM,YAAY,MAAM,EAAE,QAAS,CAAC,CAAC,SAAS,CAAC;EAC7E,IAAI,KAAK,SAAS,SAAS,GACzB,uBAAuB,IAAI,CAAC,CAAC,SAAS,SAAS;GAC7C,IAAI,QAAQ,KAAK;IAAE,WAAW,KAAK;IAAW;GAAI,CAAC;EACrD,CAAC;OAED,IAAI,QAAQ,KAAK;GAAE,WAAW,KAAK;GAAW;EAAI,CAAC;CAEvD;AACF;AAEA,SAAS,qBAAqB,UAAuD;CACnF,MAAM,iCAAgD,IAAI,IAAI;CAC9D,KAAK,MAAM,KAAK,UACd,KAAK,MAAM,KAAK,oBAAoB,YAAY,CAAC,CAAC,GAAG;EACnD,IAAI,OAAO,eAAe,IAAI,CAAC;EAC/B,IAAI,CAAC,MAAM;GACT,OAAO,CAAC;GACR,eAAe,IAAI,GAAG,IAAI;EAC5B;EACA,KAAK,KAAK,CAAC;CACb;CAEF,OAAO,CAAC,GAAG,eAAe,QAAQ,CAAC;AACrC;;;;;;;AAWA,SAAS,gBACP,UACA,WACA,SACA,QACS;CACT,MAAM,cAAc,SAAS;CAC7B,MAAM,WAAoB,IAAI,MAAM,QAAQ,MAAM;CAClD,MAAM,mBAAmB;CAEzB,MAAM,aAAa,IAAI,MAAM,WAAW;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,KAAK,WAAW,KAAK,iBAAiB,EAAE,CAAC,OAAO;CACjF,KAAK,IAAI,cAAc,GAAG,cAAc,QAAQ,QAAQ,eAAe;EACrE,MAAM,SAAS,QAAQ;EACvB,MAAM,eAAsB,IAAI,MAAM,WAAW;EACjD,KAAK,IAAI,cAAc,GAAG,cAAc,aAAa,eACnD,aAAa,eACX,OAAO,QAAQ,WAAW,cAAc,SAAS,cAAc,WAAW,MAAM,KAAK;EAEzF,SAAS,eAAe;CAC1B;CACA,OAAO;AACT;;;;;AAKA,SAAS,2BAA2B,KAAU,QAA6B;CAEzE,QAAQ,SAAS,MAAM,GAAG,EAAE,UAAU,GAAG,EAAE,OAAO,YAAY,CAAC,CAAC,SAAS,WAAW;EAClF,MAAM,EAAE,WAAW,QAAQ,WAAW,OAAO;EAC7C,IAAI,QAAQ,KAAK;GACf;GACA,SAAS,CAAC,YAAY,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,MAAO,QAAQ,IAAI,MAAM;GACpE,cAAc,CAEZ,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,GACrC,OAAO,KAAK,UAAU,MAAM,KAAK,CACnC;GACA,WAAW,KAAA;EACb,CAAC;CACH,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"EntityWriter.js","names":[],"sources":["../../src/drivers/EntityWriter.ts"],"sourcesContent":["import { getInstanceData } from \"src/BaseEntity.ts\";\nimport { type Entity } from \"src/Entity.ts\";\nimport {\n type EntityMetadata,\n type Field,\n type PrimitiveField,\n getBaseAndSelfMetas,\n getBaseSelfAndSubMetas,\n getMetadata,\n} from \"src/EntityMetadata.ts\";\nimport { getField, isChangeableField } from \"src/fields.ts\";\nimport type { Todo } from \"src/flush/Todo.ts\";\nimport { keyToNumber } from \"src/keys.ts\";\nimport { type FieldColumn, type TimestampSerde, hasSerde } from \"src/serde/fieldSerde.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\n/** A simplified view of columns, with only the keys necessary to create SQL statements. */\nexport type OpColumn = {\n columnName: string;\n dbType: string;\n isNullableArray?: boolean;\n /**\n * The unnest null/set marker as the physical db type, removed before storage.\n *\n * I.e. strings use \"\", integers use 1, and Temporal dates use \"2000-01-01\".\n * The marker must have the same physical db type as real array elements, even for empty arrays.\n *\n * Nullable arrays prepend null for SQL NULL or this marker for a present array, then pad shorter\n * arrays with nulls to make the batch rectangular. With nullable=true, unnest_arrays checks the\n * first element: null means SQL NULL; otherwise it discards that marker and removes null padding.\n * All remaining non-null elements are real values, so null elements within stored arrays are not supported.\n */\n readonly notNullMarker?: unknown;\n};\nexport type InsertOp = { tableName: string; columns: OpColumn[]; columnValues: any[][] };\n/**\n * A logical `update` operation.\n *\n * If we're using op locks, `columns` will include both the new `updatedAt` value (as bumped\n * by the `EntityManager` on mutate), as well as an extra/last \"original updated at\" column\n * (with respective pre-update values in the `rows` data), for including in the conditional\n * update clause.\n */\nexport type UpdateOp = {\n tableName: string;\n columns: OpColumn[];\n columnValues: any[][];\n updatedAt: string | undefined;\n};\nexport type DeleteOp = { tableName: string; ids: any[] };\n\ntype Ops = { inserts: InsertOp[]; updates: UpdateOp[]; deletes: DeleteOp[] };\n\n/**\n * In schemas with entity FK cycles, we may insert a dummy/null value, and later fix it up\n * before the transaction commits.\n *\n * I.e. `INSERT author.favorite_book_id` as `NULL` and then later do an `UPDATE author\n * SET favorite_book_id = 1` after the book has been inserted/we have the id.\n */\nexport type InsertFixup = {\n entity: Entity;\n tableName: string;\n column: OpColumn;\n value: any;\n};\n\n/**\n * Builds AST-ish `Ops` for the inserts/updates/deletes in `todos`.\n *\n * This helps decouple the database-specific driver/library from the knowledge\n * of turning entities into table operations, i.e. so they can be unaware of\n * complexities like field -> column mapping, oplocks, and class table inheritance.\n *\n * We still assume Postgres b/c we assume all ids have been pre-assigned for INSERTs,\n * and don't do any topological sorting of cross-row FK dependencies.\n *\n * But this should let us experiment with knex vs. raw client/etc.\n */\nexport function generateOps(todos: Record<string, Todo>): Ops {\n const ops: Ops = { inserts: [], updates: [], deletes: [] };\n const fixups: InsertFixup[] = [];\n // Would be nice to conditionally sort\n const sorted = Object.values(todos).sort(\n (a, b) => (a.metadata.nonDeferredFkOrder ?? 0) - (b.metadata.nonDeferredFkOrder ?? 0),\n );\n for (const todo of sorted) {\n addInserts(ops, todo, fixups);\n addUpdates(ops, todo);\n addDeletes(ops, todo);\n }\n if (fixups.length > 0) translateFixupsIntoUpdates(ops, fixups);\n return ops;\n}\n\nfunction addInserts(ops: Ops, todo: Todo, fixups: InsertFixup[]): void {\n if (todo.inserts.length > 0) {\n // If we have subtypes, this todo.metadata will always be the base type\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n // Insert into each of the CTI tables\n for (const [meta, group] of groupEntitiesByTable(todo.inserts)) {\n ops.inserts.push(newInsertOp(meta, group, fixups));\n }\n } else if (meta.inheritanceType === \"sti\") {\n // One INSERT per subtype, so each statement's column list spans exactly what that subtype can\n // set. A single statement across subtypes has to bind NULL for the other subtypes' columns,\n // which defeats their database defaults, and outright fails for notNull ones.\n for (const group of groupBy(todo.inserts, (e) => getMetadata(e)).values()) {\n ops.inserts.push(newStiInsertOp(meta, group, fixups));\n }\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n ops.inserts.push(newInsertOp(meta, todo.inserts, fixups));\n }\n }\n}\n\nfunction newInsertOp(meta: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // Purposefully use `meta.fields` instead of `allFields` b/c each CTI table has its own `InsertOp`\n const columns = Object.values(meta.fields)\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, meta.tableName, columns, fixups);\n return { tableName: meta.tableName, columns, columnValues };\n}\n\nfunction newStiInsertOp(root: EntityMetadata, entities: Entity[], fixups: InsertFixup[]): InsertOp {\n // `allFields` is the base's fields plus this subtype's own, i.e. everything this subtype can write.\n // Anything it can't write is left out of the statement entirely, so the database applies its default.\n const fields: Field[] = Object.values(getMetadata(entities[0]).allFields);\n const columns = fields.filter(hasSerde).flatMap((f) => f.serde.columns);\n const columnValues = collectBindings(entities, root.tableName, columns, fixups);\n return { tableName: root.tableName, columns, columnValues };\n}\n\nfunction addUpdates(ops: Ops, todo: Todo): void {\n if (todo.updates.length > 0) {\n const meta = todo.metadata;\n if (meta.subTypes.length > 0) {\n if (meta.inheritanceType === \"cti\") {\n for (const [meta, group] of groupEntitiesByTable(todo.updates)) {\n const op = newUpdateOp(meta, group);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, group);\n }\n } else if (meta.inheritanceType === \"sti\") {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n } else {\n throw new Error(`Found ${meta.tableName} subTypes without a known inheritanceType ${meta.inheritanceType}`);\n }\n } else {\n const op = newUpdateOp(meta, todo.updates);\n if (op) ops.updates.push(op);\n addLazyUpdates(ops, meta, todo.updates);\n }\n }\n}\n\n/**\n * Writes changed `lazy` columns via a targeted `UPDATE table SET lazy_col = ... WHERE id in (...)`.\n *\n * Lazy columns are excluded from the main batch UPDATE (which spans the batch's union of changed fields),\n * so that entities in the batch that never loaded a lazy column keep their existing db value instead of\n * being overwritten with null. Here we write each lazy column only for the entities that actually changed it.\n */\nfunction addLazyUpdates(ops: Ops, meta: EntityMetadata, entities: Entity[]): void {\n const idColumn = meta.fields[\"id\"].serde!.columns[0];\n for (const fieldName of meta.lazyFieldNames!) {\n const field = meta.fields[fieldName];\n if (!hasSerde(field)) continue;\n const changed = entities.filter((e) => fieldName in getInstanceData(e).changedData);\n if (changed.length === 0) continue;\n const columns: BindingColumn[] = [idColumn, ...field.serde.columns];\n const columnValues = collectBindings(changed, meta.tableName, columns, undefined);\n ops.updates.push({ tableName: meta.tableName, columns, columnValues, updatedAt: undefined });\n }\n}\n\nfunction newUpdateOp(meta: EntityMetadata, entities: Entity[]): UpdateOp | undefined {\n // We only include changed fields in our `UPDATE`--maybe we could change this\n // to always use the same fields, to take advantage of Prepared Statements.\n const changedFields = new Set<string>();\n for (const entity of entities) {\n for (const fieldName in getInstanceData(entity).changedData) changedFields.add(fieldName);\n }\n // Sometimes with derived fields, an instance will be marked as an update, but if the derived field hasn't\n // actually changed, it'll be a noop, so just short-circuit if it looks like that happened. Unless touched.\n if (changedFields.size === 0 && !entities.some((e) => getInstanceData(e).isTouched)) {\n return undefined;\n }\n\n // We may have loaded a1 and a2, and changed a1.firstName, and a2.lastName, but either one\n // might be missing the other's changed fields in it's lazy-initialized data field...\n // (Use `?` because subtypes won't have the updatedAt, and it will be handled by the base type\n // `newUpdateUp`--except STI where we're doing it all in one go, probably needs checked here.)\n const updatedAt = meta.timestampFields?.updatedAt;\n changedFields.add(\"id\");\n if (updatedAt) changedFields.add(updatedAt);\n // `lazy` columns are deliberately excluded from the batch UPDATE and written by `addLazyUpdates`; an\n // entity that never loaded a lazy column isn't in `row`, so forcing `getField` here would clobber it to null.\n const lazyFieldNames = meta.lazyFieldNames!;\n for (const entity of entities) {\n const { data } = getInstanceData(entity);\n for (const key of changedFields) {\n // Check isChangeableField because we might be updating the base `publishers` table\n // and `originalData` might have fields from a subclass `large_publishers` table.\n if (!(key in data) && isChangeableField(entity, key) && !lazyFieldNames.has(key)) {\n getField(entity, key, true);\n }\n }\n }\n\n const columns: Array<BindingColumn> = (\n meta.inheritanceType === \"sti\"\n ? // Hack this one handling of STI into here...\n getBaseSelfAndSubMetas(meta).flatMap((meta) =>\n meta.stiDiscriminatorField\n ? Object.values(meta.fields)\n : Object.values(meta.fields).filter((f) => f.fieldName !== \"id\"),\n )\n : Object.values(meta.fields)\n )\n .filter((f) => changedFields.has(f.fieldName))\n .filter((f) => !lazyFieldNames.has(f.fieldName))\n .filter(hasSerde)\n .flatMap((f) => f.serde.columns);\n\n // If we're using class table inheritance, base/child tables may not have any columns to update\n if (columns.length === 1) {\n return undefined;\n }\n\n // We already have the bumped updated_at column, but also include the original updated_at for the data CTE\n if (updatedAt) {\n const serde = meta.fields[updatedAt].serde as TimestampSerde<unknown>;\n columns.push({\n columnName: \"__original_updated_at\",\n dbType: \"timestamptz\",\n dbValue: (_, entity) => serde.dbValue(getInstanceData(entity).originalData),\n });\n }\n\n const columnValues = collectBindings(entities, meta.tableName, columns, []);\n const updatedAtColumn = updatedAt ? (meta.fields[updatedAt] as PrimitiveField).serde.columns[0] : undefined;\n return { tableName: meta.tableName, columns, updatedAt: updatedAtColumn?.columnName, columnValues };\n}\n\nfunction addDeletes(ops: Ops, todo: Todo): void {\n if (todo.deletes.length > 0) {\n const meta = todo.metadata;\n const ids = todo.deletes.map((e) => keyToNumber(meta, e.idTagged!).toString());\n if (meta.subTypes.length > 0) {\n getBaseSelfAndSubMetas(meta).forEach((meta) => {\n ops.deletes.push({ tableName: meta.tableName, ids });\n });\n } else {\n ops.deletes.push({ tableName: meta.tableName, ids });\n }\n }\n}\n\nfunction groupEntitiesByTable(entities: Entity[]): Array<[EntityMetadata, Entity[]]> {\n const entitiesByType: Map<EntityMetadata, Entity[]> = new Map();\n for (const e of entities) {\n for (const m of getBaseAndSelfMetas(getMetadata(e))) {\n let list = entitiesByType.get(m);\n if (!list) {\n list = [];\n entitiesByType.set(m, list);\n }\n list.push(e);\n }\n }\n return [...entitiesByType.entries()];\n}\n\ntype BindingColumn = OpColumn & Pick<FieldColumn, \"dbValue\">;\ntype EntityWithData = Entity & { __data: { data: Record<string, unknown> } };\n\n/**\n * Builds a *columnar* array of bindings for the given `entities` and `columns`.\n *\n * We use a columnar approach, instead of `rows[]`, to best match the unnest-based\n * column operations in our `batchInsert` / `batchUpdate` SQL statements.\n */\nfunction collectBindings(\n entities: Entity[],\n tableName: string,\n columns: BindingColumn[],\n fixups: InsertFixup[] | undefined,\n): any[][] {\n const entityCount = entities.length;\n const bindings: any[][] = new Array(columns.length);\n const entitiesWithData = entities as EntityWithData[];\n // Cache data separately; this benchmarks faster than `entities[i].__data.data` in the inner loop.\n const entityData = new Array(entityCount);\n for (let i = 0; i < entityCount; i++) entityData[i] = entitiesWithData[i].__data.data;\n for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n const column = columns[columnIndex];\n const columnValues: any[] = new Array(entityCount);\n for (let entityIndex = 0; entityIndex < entityCount; entityIndex++) {\n columnValues[entityIndex] =\n column.dbValue(entityData[entityIndex], entities[entityIndex], tableName, fixups) ?? null;\n }\n bindings[columnIndex] = columnValues;\n }\n return bindings;\n}\n/**\n * Given `fixups` that indicate where we inserted `NULL` into non-deferred FKs, create `UPDATE`s\n * that insert the value now that the FK check will succeed.\n */\nfunction translateFixupsIntoUpdates(ops: Ops, fixups: InsertFixup[]): void {\n // Create a single `UPDATE` for the N fixups we might have for each table/column combination\n groupBy(fixups, (f) => `${f.tableName}.${f.column.columnName}`).forEach((fixups) => {\n const { tableName, entity, column } = fixups[0];\n ops.updates.push({\n tableName,\n columns: [getMetadata(entity).fields[\"id\"].serde!.columns[0], column],\n columnValues: [\n // Make 1 column of ids, and 1 column of values\n fixups.map((fixup) => fixup.entity.id),\n fixups.map((fixup) => fixup.value),\n ],\n updatedAt: undefined,\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+EA,SAAgB,YAAY,OAAkC;CAC5D,MAAM,MAAW;EAAE,SAAS,CAAC;EAAG,SAAS,CAAC;EAAG,SAAS,CAAC;CAAE;CACzD,MAAM,SAAwB,CAAC;CAE/B,MAAM,SAAS,OAAO,OAAO,KAAK,CAAC,CAAC,MACjC,GAAG,OAAO,EAAE,SAAS,sBAAsB,MAAM,EAAE,SAAS,sBAAsB,EACrF;CACA,KAAK,MAAM,QAAQ,QAAQ;EACzB,WAAW,KAAK,MAAM,MAAM;EAC5B,WAAW,KAAK,IAAI;EACpB,WAAW,KAAK,IAAI;CACtB;CACA,IAAI,OAAO,SAAS,GAAG,2BAA2B,KAAK,MAAM;CAC7D,OAAO;AACT;AAEA,SAAS,WAAW,KAAU,MAAY,QAA6B;CACrE,IAAI,KAAK,QAAQ,SAAS,GAAG;EAE3B,MAAM,OAAO,KAAK;EAClB,IAAI,KAAK,SAAS,SAAS,GAAG;GAC5B,IAAI,KAAK,oBAAoB,OAE3B,KAAK,MAAM,CAAC,MAAM,UAAU,qBAAqB,KAAK,OAAO,GAC3D,IAAI,QAAQ,KAAK,YAAY,MAAM,OAAO,MAAM,CAAC;QAE9C,IAAI,KAAK,oBAAoB,OAIlC,KAAK,MAAM,SAAS,QAAQ,KAAK,UAAU,MAAM,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,GACtE,IAAI,QAAQ,KAAK,eAAe,MAAM,OAAO,MAAM,CAAC;QAGtD,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,4CAA4C,KAAK,iBAAiB;EAE9G,OACE,IAAI,QAAQ,KAAK,YAAY,MAAM,KAAK,SAAS,MAAM,CAAC;CAE5D;AACF;AAEA,SAAS,YAAY,MAAsB,UAAoB,QAAiC;CAE9F,MAAM,UAAU,OAAO,OAAO,KAAK,MAAM,CAAC,CACvC,OAAO,QAAQ,CAAC,CAChB,SAAS,MAAM,EAAE,MAAM,OAAO;CACjC,MAAM,eAAe,gBAAgB,UAAU,KAAK,WAAW,SAAS,MAAM;CAC9E,OAAO;EAAE,WAAW,KAAK;EAAW;EAAS;CAAa;AAC5D;AAEA,SAAS,eAAe,MAAsB,UAAoB,QAAiC;CAIjG,MAAM,UADkB,OAAO,OAAO,YAAY,SAAS,EAAE,CAAC,CAAC,SAC1C,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,OAAO;CACtE,MAAM,eAAe,gBAAgB,UAAU,KAAK,WAAW,SAAS,MAAM;CAC9E,OAAO;EAAE,WAAW,KAAK;EAAW;EAAS;CAAa;AAC5D;AAEA,SAAS,WAAW,KAAU,MAAkB;CAC9C,IAAI,KAAK,QAAQ,SAAS,GAAG;EAC3B,MAAM,OAAO,KAAK;EAClB,IAAI,KAAK,SAAS,SAAS,GAAG;GAC5B,IAAI,KAAK,oBAAoB,OAC3B,KAAK,MAAM,CAAC,MAAM,UAAU,qBAAqB,KAAK,OAAO,GAAG;IAC9D,MAAM,KAAK,YAAY,MAAM,KAAK;IAClC,IAAI,IAAI,IAAI,QAAQ,KAAK,EAAE;IAC3B,eAAe,KAAK,MAAM,KAAK;GACjC;QACK,IAAI,KAAK,oBAAoB,OAAO;IACzC,MAAM,KAAK,YAAY,MAAM,KAAK,OAAO;IACzC,IAAI,IAAI,IAAI,QAAQ,KAAK,EAAE;IAC3B,eAAe,KAAK,MAAM,KAAK,OAAO;GACxC,OACE,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,4CAA4C,KAAK,iBAAiB;EAE9G,OAAO;GACL,MAAM,KAAK,YAAY,MAAM,KAAK,OAAO;GACzC,IAAI,IAAI,IAAI,QAAQ,KAAK,EAAE;GAC3B,eAAe,KAAK,MAAM,KAAK,OAAO;EACxC;CACF;AACF;;;;;;;;AASA,SAAS,eAAe,KAAU,MAAsB,UAA0B;CAChF,MAAM,WAAW,KAAK,OAAO,KAAK,CAAC,MAAO,QAAQ;CAClD,KAAK,MAAM,aAAa,KAAK,gBAAiB;EAC5C,MAAM,QAAQ,KAAK,OAAO;EAC1B,IAAI,CAAC,SAAS,KAAK,GAAG;EACtB,MAAM,UAAU,SAAS,QAAQ,MAAM,aAAa,gBAAgB,CAAC,CAAC,CAAC,WAAW;EAClF,IAAI,QAAQ,WAAW,GAAG;EAC1B,MAAM,UAA2B,CAAC,UAAU,GAAG,MAAM,MAAM,OAAO;EAClE,MAAM,eAAe,gBAAgB,SAAS,KAAK,WAAW,SAAS,KAAA,CAAS;EAChF,IAAI,QAAQ,KAAK;GAAE,WAAW,KAAK;GAAW;GAAS;GAAc,WAAW,KAAA;EAAU,CAAC;CAC7F;AACF;AAEA,SAAS,YAAY,MAAsB,UAA0C;CAGnF,MAAM,gCAAgB,IAAI,IAAY;CACtC,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,aAAa,gBAAgB,MAAM,CAAC,CAAC,aAAa,cAAc,IAAI,SAAS;CAI1F,IAAI,cAAc,SAAS,KAAK,CAAC,SAAS,MAAM,MAAM,gBAAgB,CAAC,CAAC,CAAC,SAAS,GAChF;CAOF,MAAM,YAAY,KAAK,iBAAiB;CACxC,cAAc,IAAI,IAAI;CACtB,IAAI,WAAW,cAAc,IAAI,SAAS;CAG1C,MAAM,iBAAiB,KAAK;CAC5B,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,EAAE,SAAS,gBAAgB,MAAM;EACvC,KAAK,MAAM,OAAO,eAGhB,IAAI,EAAE,OAAO,SAAS,kBAAkB,QAAQ,GAAG,KAAK,CAAC,eAAe,IAAI,GAAG,GAC7E,SAAS,QAAQ,KAAK,IAAI;CAGhC;CAEA,MAAM,WACJ,KAAK,oBAAoB,QAErB,uBAAuB,IAAI,CAAC,CAAC,SAAS,SACpC,KAAK,wBACD,OAAO,OAAO,KAAK,MAAM,IACzB,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,QAAQ,MAAM,EAAE,cAAc,IAAI,CACnE,IACA,OAAO,OAAO,KAAK,MAAM,EAAA,CAE5B,QAAQ,MAAM,cAAc,IAAI,EAAE,SAAS,CAAC,CAAC,CAC7C,QAAQ,MAAM,CAAC,eAAe,IAAI,EAAE,SAAS,CAAC,CAAC,CAC/C,OAAO,QAAQ,CAAC,CAChB,SAAS,MAAM,EAAE,MAAM,OAAO;CAGjC,IAAI,QAAQ,WAAW,GACrB;CAIF,IAAI,WAAW;EACb,MAAM,QAAQ,KAAK,OAAO,UAAU,CAAC;EACrC,QAAQ,KAAK;GACX,YAAY;GACZ,QAAQ;GACR,UAAU,GAAG,WAAW,MAAM,QAAQ,gBAAgB,MAAM,CAAC,CAAC,YAAY;EAC5E,CAAC;CACH;CAEA,MAAM,eAAe,gBAAgB,UAAU,KAAK,WAAW,SAAS,CAAC,CAAC;CAC1E,MAAM,kBAAkB,YAAa,KAAK,OAAO,UAAU,CAAoB,MAAM,QAAQ,KAAK,KAAA;CAClG,OAAO;EAAE,WAAW,KAAK;EAAW;EAAS,WAAW,iBAAiB;EAAY;CAAa;AACpG;AAEA,SAAS,WAAW,KAAU,MAAkB;CAC9C,IAAI,KAAK,QAAQ,SAAS,GAAG;EAC3B,MAAM,OAAO,KAAK;EAClB,MAAM,MAAM,KAAK,QAAQ,KAAK,MAAM,YAAY,MAAM,EAAE,QAAS,CAAC,CAAC,SAAS,CAAC;EAC7E,IAAI,KAAK,SAAS,SAAS,GACzB,uBAAuB,IAAI,CAAC,CAAC,SAAS,SAAS;GAC7C,IAAI,QAAQ,KAAK;IAAE,WAAW,KAAK;IAAW;GAAI,CAAC;EACrD,CAAC;OAED,IAAI,QAAQ,KAAK;GAAE,WAAW,KAAK;GAAW;EAAI,CAAC;CAEvD;AACF;AAEA,SAAS,qBAAqB,UAAuD;CACnF,MAAM,iCAAgD,IAAI,IAAI;CAC9D,KAAK,MAAM,KAAK,UACd,KAAK,MAAM,KAAK,oBAAoB,YAAY,CAAC,CAAC,GAAG;EACnD,IAAI,OAAO,eAAe,IAAI,CAAC;EAC/B,IAAI,CAAC,MAAM;GACT,OAAO,CAAC;GACR,eAAe,IAAI,GAAG,IAAI;EAC5B;EACA,KAAK,KAAK,CAAC;CACb;CAEF,OAAO,CAAC,GAAG,eAAe,QAAQ,CAAC;AACrC;;;;;;;AAWA,SAAS,gBACP,UACA,WACA,SACA,QACS;CACT,MAAM,cAAc,SAAS;CAC7B,MAAM,WAAoB,IAAI,MAAM,QAAQ,MAAM;CAClD,MAAM,mBAAmB;CAEzB,MAAM,aAAa,IAAI,MAAM,WAAW;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,KAAK,WAAW,KAAK,iBAAiB,EAAE,CAAC,OAAO;CACjF,KAAK,IAAI,cAAc,GAAG,cAAc,QAAQ,QAAQ,eAAe;EACrE,MAAM,SAAS,QAAQ;EACvB,MAAM,eAAsB,IAAI,MAAM,WAAW;EACjD,KAAK,IAAI,cAAc,GAAG,cAAc,aAAa,eACnD,aAAa,eACX,OAAO,QAAQ,WAAW,cAAc,SAAS,cAAc,WAAW,MAAM,KAAK;EAEzF,SAAS,eAAe;CAC1B;CACA,OAAO;AACT;;;;;AAKA,SAAS,2BAA2B,KAAU,QAA6B;CAEzE,QAAQ,SAAS,MAAM,GAAG,EAAE,UAAU,GAAG,EAAE,OAAO,YAAY,CAAC,CAAC,SAAS,WAAW;EAClF,MAAM,EAAE,WAAW,QAAQ,WAAW,OAAO;EAC7C,IAAI,QAAQ,KAAK;GACf;GACA,SAAS,CAAC,YAAY,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,MAAO,QAAQ,IAAI,MAAM;GACpE,cAAc,CAEZ,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,GACrC,OAAO,KAAK,UAAU,MAAM,KAAK,CACnC;GACA,WAAW,KAAA;EACb,CAAC;CACH,CAAC;AACH"}
|
package/build/queries/unnest.cjs
CHANGED
|
@@ -48,12 +48,11 @@ function fillArrayWithNulls(c, array, maxSize) {
|
|
|
48
48
|
if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));
|
|
49
49
|
return result;
|
|
50
50
|
}
|
|
51
|
+
/** Return the unnest null/set marker as the physical db type, rejecting null or undefined. */
|
|
51
52
|
function notNullMarker(c) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else if (c.dbType.startsWith("timestamp")) return /* @__PURE__ */ new Date();
|
|
56
|
-
else throw new Error("Unhandled nullable array column type " + c.dbType);
|
|
53
|
+
const marker = c.notNullMarker;
|
|
54
|
+
if (marker === null || marker === void 0) throw new Error("Missing non-null array marker for column " + c.columnName);
|
|
55
|
+
return marker;
|
|
57
56
|
}
|
|
58
57
|
//#endregion
|
|
59
58
|
exports.buildUnnestCte = buildUnnestCte;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unnest.cjs","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { ParsedCteClause } from \"src/queries/find/QueryParser.ts\";\n\n/**\n * Creates a CTE named `alias` that bulk-injects the `columnValues` into a SQL query.\n *\n * I.e. `columns = [first_name, last_name]` means columnValues would be all first names,\n * then all last names, i.e.`[[\"f1\",\"f2\"], [\"l1\",\"l2\"]]`.\n *\n * Note that `columnValues` will be mutated if any array columns need to be made rectangular.\n */\nexport function buildUnnestCte(alias: string, columns: OpColumn[], columnValues: any[][]): ParsedCteClause {\n // Make any arrays-of-arrays rectangular by filling them with nulls\n ensureRectangularArraySizes(columns, columnValues);\n // Use unnest or unnest_arrays depending on whether the column is an array column\n const selects = columns.map((c) => {\n if (c.dbType.endsWith(\"[]\")) {\n if (c.isNullableArray) {\n return `unnest_arrays(?::${c.dbType}[], true)`;\n } else {\n return `unnest_arrays(?::${c.dbType}[])`;\n }\n } else {\n return `unnest(?::${c.dbType}[])`;\n }\n });\n return {\n alias,\n columns,\n query: { kind: \"raw\", sql: `SELECT ${selects.join(\", \")}`, bindings: columnValues },\n };\n}\n\nexport function ensureRectangularArraySizes(columns: OpColumn[], columnValues: any[][]): void {\n for (let i = 0; i < columns.length; i++) {\n const column = columns[i];\n if (column.dbType.endsWith(\"[]\")) {\n const values = columnValues[i];\n const fillTo = findArrayMaxSize(values);\n for (let j = 0; j < values.length; j++) {\n values[j] = fillArrayWithNulls(column, values[j], fillTo);\n }\n }\n }\n}\n\nfunction findArrayMaxSize(columnValues: readonly any[][]): number {\n let max = 1; // postgres really doesn't like `{{}}` so always send at least 1 element\n for (let i = 0; i < columnValues.length; i++) max = Math.max(max, columnValues[i]?.length ?? 0);\n return max;\n}\n\n// Because postgres array-of-arrays must be rectangular, we fill all arrays up the same max size\n// to put on the wire, and then later strip the padded nulls during the unnest_arrays call.\nfunction fillArrayWithNulls(c: OpColumn, array: any[] | null, maxSize: number): any[] {\n const wasNull = array === null;\n const result = array ? [...array] : [];\n const nullsToAdd = Math.max(0, maxSize - (array?.length ?? 0));\n for (let i = 0; i < nullsToAdd; i++) result.push(null);\n // Add our unset marker\n if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));\n return result;\n}\n\
|
|
1
|
+
{"version":3,"file":"unnest.cjs","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { ParsedCteClause } from \"src/queries/find/QueryParser.ts\";\n\n/**\n * Creates a CTE named `alias` that bulk-injects the `columnValues` into a SQL query.\n *\n * I.e. `columns = [first_name, last_name]` means columnValues would be all first names,\n * then all last names, i.e.`[[\"f1\",\"f2\"], [\"l1\",\"l2\"]]`.\n *\n * Note that `columnValues` will be mutated if any array columns need to be made rectangular.\n */\nexport function buildUnnestCte(alias: string, columns: OpColumn[], columnValues: any[][]): ParsedCteClause {\n // Make any arrays-of-arrays rectangular by filling them with nulls\n ensureRectangularArraySizes(columns, columnValues);\n // Use unnest or unnest_arrays depending on whether the column is an array column\n const selects = columns.map((c) => {\n if (c.dbType.endsWith(\"[]\")) {\n if (c.isNullableArray) {\n return `unnest_arrays(?::${c.dbType}[], true)`;\n } else {\n return `unnest_arrays(?::${c.dbType}[])`;\n }\n } else {\n return `unnest(?::${c.dbType}[])`;\n }\n });\n return {\n alias,\n columns,\n query: { kind: \"raw\", sql: `SELECT ${selects.join(\", \")}`, bindings: columnValues },\n };\n}\n\nexport function ensureRectangularArraySizes(columns: OpColumn[], columnValues: any[][]): void {\n for (let i = 0; i < columns.length; i++) {\n const column = columns[i];\n if (column.dbType.endsWith(\"[]\")) {\n const values = columnValues[i];\n const fillTo = findArrayMaxSize(values);\n for (let j = 0; j < values.length; j++) {\n values[j] = fillArrayWithNulls(column, values[j], fillTo);\n }\n }\n }\n}\n\nfunction findArrayMaxSize(columnValues: readonly any[][]): number {\n let max = 1; // postgres really doesn't like `{{}}` so always send at least 1 element\n for (let i = 0; i < columnValues.length; i++) max = Math.max(max, columnValues[i]?.length ?? 0);\n return max;\n}\n\n// Because postgres array-of-arrays must be rectangular, we fill all arrays up the same max size\n// to put on the wire, and then later strip the padded nulls during the unnest_arrays call.\nfunction fillArrayWithNulls(c: OpColumn, array: any[] | null, maxSize: number): any[] {\n const wasNull = array === null;\n const result = array ? [...array] : [];\n const nullsToAdd = Math.max(0, maxSize - (array?.length ?? 0));\n for (let i = 0; i < nullsToAdd; i++) result.push(null);\n // Add our unset marker for unnest\n if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));\n return result;\n}\n\n/** Return the unnest null/set marker as the physical db type, rejecting null or undefined. */\nfunction notNullMarker(c: OpColumn): unknown {\n const marker = c.notNullMarker;\n if (marker === null || marker === undefined) {\n throw new Error(\"Missing non-null array marker for column \" + c.columnName);\n }\n return marker;\n}\n"],"mappings":";;;;;;;;;;AAWA,SAAgB,eAAe,OAAe,SAAqB,cAAwC;CAEzG,4BAA4B,SAAS,YAAY;CAajD,OAAO;EACL;EACA;EACA,OAAO;GAAE,MAAM;GAAO,KAAK,UAdb,QAAQ,KAAK,MAAM;IACjC,IAAI,EAAE,OAAO,SAAS,IAAI,GAAG;KAC3B,IAAI,EAAE,iBACJ,OAAO,oBAAoB,EAAE,OAAO;UAEpC,OAAO,oBAAoB,EAAE,OAAO;IAExC,OACE,OAAO,aAAa,EAAE,OAAO;GAEjC,CAI6C,CAAC,CAAC,KAAK,IAAI;GAAK,UAAU;EAAa;CACpF;AACF;AAEA,SAAgB,4BAA4B,SAAqB,cAA6B;CAC5F,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,SAAS,QAAQ;EACvB,IAAI,OAAO,OAAO,SAAS,IAAI,GAAG;GAChC,MAAM,SAAS,aAAa;GAC5B,MAAM,SAAS,iBAAiB,MAAM;GACtC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KACjC,OAAO,KAAK,mBAAmB,QAAQ,OAAO,IAAI,MAAM;EAE5D;CACF;AACF;AAEA,SAAS,iBAAiB,cAAwC;CAChE,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,aAAa,EAAE,EAAE,UAAU,CAAC;CAC9F,OAAO;AACT;AAIA,SAAS,mBAAmB,GAAa,OAAqB,SAAwB;CACpF,MAAM,UAAU,UAAU;CAC1B,MAAM,SAAS,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;CACrC,MAAM,aAAa,KAAK,IAAI,GAAG,WAAW,OAAO,UAAU,EAAE;CAC7D,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK,OAAO,KAAK,IAAI;CAErD,IAAI,EAAE,iBAAiB,OAAO,QAAQ,UAAU,OAAO,cAAc,CAAC,CAAC;CACvE,OAAO;AACT;;AAGA,SAAS,cAAc,GAAsB;CAC3C,MAAM,SAAS,EAAE;CACjB,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,MAAM,IAAI,MAAM,8CAA8C,EAAE,UAAU;CAE5E,OAAO;AACT"}
|
package/build/queries/unnest.js
CHANGED
|
@@ -47,12 +47,11 @@ function fillArrayWithNulls(c, array, maxSize) {
|
|
|
47
47
|
if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));
|
|
48
48
|
return result;
|
|
49
49
|
}
|
|
50
|
+
/** Return the unnest null/set marker as the physical db type, rejecting null or undefined. */
|
|
50
51
|
function notNullMarker(c) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
else if (c.dbType.startsWith("timestamp")) return /* @__PURE__ */ new Date();
|
|
55
|
-
else throw new Error("Unhandled nullable array column type " + c.dbType);
|
|
52
|
+
const marker = c.notNullMarker;
|
|
53
|
+
if (marker === null || marker === void 0) throw new Error("Missing non-null array marker for column " + c.columnName);
|
|
54
|
+
return marker;
|
|
56
55
|
}
|
|
57
56
|
//#endregion
|
|
58
57
|
export { buildUnnestCte, ensureRectangularArraySizes };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unnest.js","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { ParsedCteClause } from \"src/queries/find/QueryParser.ts\";\n\n/**\n * Creates a CTE named `alias` that bulk-injects the `columnValues` into a SQL query.\n *\n * I.e. `columns = [first_name, last_name]` means columnValues would be all first names,\n * then all last names, i.e.`[[\"f1\",\"f2\"], [\"l1\",\"l2\"]]`.\n *\n * Note that `columnValues` will be mutated if any array columns need to be made rectangular.\n */\nexport function buildUnnestCte(alias: string, columns: OpColumn[], columnValues: any[][]): ParsedCteClause {\n // Make any arrays-of-arrays rectangular by filling them with nulls\n ensureRectangularArraySizes(columns, columnValues);\n // Use unnest or unnest_arrays depending on whether the column is an array column\n const selects = columns.map((c) => {\n if (c.dbType.endsWith(\"[]\")) {\n if (c.isNullableArray) {\n return `unnest_arrays(?::${c.dbType}[], true)`;\n } else {\n return `unnest_arrays(?::${c.dbType}[])`;\n }\n } else {\n return `unnest(?::${c.dbType}[])`;\n }\n });\n return {\n alias,\n columns,\n query: { kind: \"raw\", sql: `SELECT ${selects.join(\", \")}`, bindings: columnValues },\n };\n}\n\nexport function ensureRectangularArraySizes(columns: OpColumn[], columnValues: any[][]): void {\n for (let i = 0; i < columns.length; i++) {\n const column = columns[i];\n if (column.dbType.endsWith(\"[]\")) {\n const values = columnValues[i];\n const fillTo = findArrayMaxSize(values);\n for (let j = 0; j < values.length; j++) {\n values[j] = fillArrayWithNulls(column, values[j], fillTo);\n }\n }\n }\n}\n\nfunction findArrayMaxSize(columnValues: readonly any[][]): number {\n let max = 1; // postgres really doesn't like `{{}}` so always send at least 1 element\n for (let i = 0; i < columnValues.length; i++) max = Math.max(max, columnValues[i]?.length ?? 0);\n return max;\n}\n\n// Because postgres array-of-arrays must be rectangular, we fill all arrays up the same max size\n// to put on the wire, and then later strip the padded nulls during the unnest_arrays call.\nfunction fillArrayWithNulls(c: OpColumn, array: any[] | null, maxSize: number): any[] {\n const wasNull = array === null;\n const result = array ? [...array] : [];\n const nullsToAdd = Math.max(0, maxSize - (array?.length ?? 0));\n for (let i = 0; i < nullsToAdd; i++) result.push(null);\n // Add our unset marker\n if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));\n return result;\n}\n\
|
|
1
|
+
{"version":3,"file":"unnest.js","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { ParsedCteClause } from \"src/queries/find/QueryParser.ts\";\n\n/**\n * Creates a CTE named `alias` that bulk-injects the `columnValues` into a SQL query.\n *\n * I.e. `columns = [first_name, last_name]` means columnValues would be all first names,\n * then all last names, i.e.`[[\"f1\",\"f2\"], [\"l1\",\"l2\"]]`.\n *\n * Note that `columnValues` will be mutated if any array columns need to be made rectangular.\n */\nexport function buildUnnestCte(alias: string, columns: OpColumn[], columnValues: any[][]): ParsedCteClause {\n // Make any arrays-of-arrays rectangular by filling them with nulls\n ensureRectangularArraySizes(columns, columnValues);\n // Use unnest or unnest_arrays depending on whether the column is an array column\n const selects = columns.map((c) => {\n if (c.dbType.endsWith(\"[]\")) {\n if (c.isNullableArray) {\n return `unnest_arrays(?::${c.dbType}[], true)`;\n } else {\n return `unnest_arrays(?::${c.dbType}[])`;\n }\n } else {\n return `unnest(?::${c.dbType}[])`;\n }\n });\n return {\n alias,\n columns,\n query: { kind: \"raw\", sql: `SELECT ${selects.join(\", \")}`, bindings: columnValues },\n };\n}\n\nexport function ensureRectangularArraySizes(columns: OpColumn[], columnValues: any[][]): void {\n for (let i = 0; i < columns.length; i++) {\n const column = columns[i];\n if (column.dbType.endsWith(\"[]\")) {\n const values = columnValues[i];\n const fillTo = findArrayMaxSize(values);\n for (let j = 0; j < values.length; j++) {\n values[j] = fillArrayWithNulls(column, values[j], fillTo);\n }\n }\n }\n}\n\nfunction findArrayMaxSize(columnValues: readonly any[][]): number {\n let max = 1; // postgres really doesn't like `{{}}` so always send at least 1 element\n for (let i = 0; i < columnValues.length; i++) max = Math.max(max, columnValues[i]?.length ?? 0);\n return max;\n}\n\n// Because postgres array-of-arrays must be rectangular, we fill all arrays up the same max size\n// to put on the wire, and then later strip the padded nulls during the unnest_arrays call.\nfunction fillArrayWithNulls(c: OpColumn, array: any[] | null, maxSize: number): any[] {\n const wasNull = array === null;\n const result = array ? [...array] : [];\n const nullsToAdd = Math.max(0, maxSize - (array?.length ?? 0));\n for (let i = 0; i < nullsToAdd; i++) result.push(null);\n // Add our unset marker for unnest\n if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));\n return result;\n}\n\n/** Return the unnest null/set marker as the physical db type, rejecting null or undefined. */\nfunction notNullMarker(c: OpColumn): unknown {\n const marker = c.notNullMarker;\n if (marker === null || marker === undefined) {\n throw new Error(\"Missing non-null array marker for column \" + c.columnName);\n }\n return marker;\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,eAAe,OAAe,SAAqB,cAAwC;CAEzG,4BAA4B,SAAS,YAAY;CAajD,OAAO;EACL;EACA;EACA,OAAO;GAAE,MAAM;GAAO,KAAK,UAdb,QAAQ,KAAK,MAAM;IACjC,IAAI,EAAE,OAAO,SAAS,IAAI,GAAG;KAC3B,IAAI,EAAE,iBACJ,OAAO,oBAAoB,EAAE,OAAO;UAEpC,OAAO,oBAAoB,EAAE,OAAO;IAExC,OACE,OAAO,aAAa,EAAE,OAAO;GAEjC,CAI6C,CAAC,CAAC,KAAK,IAAI;GAAK,UAAU;EAAa;CACpF;AACF;AAEA,SAAgB,4BAA4B,SAAqB,cAA6B;CAC5F,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,SAAS,QAAQ;EACvB,IAAI,OAAO,OAAO,SAAS,IAAI,GAAG;GAChC,MAAM,SAAS,aAAa;GAC5B,MAAM,SAAS,iBAAiB,MAAM;GACtC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KACjC,OAAO,KAAK,mBAAmB,QAAQ,OAAO,IAAI,MAAM;EAE5D;CACF;AACF;AAEA,SAAS,iBAAiB,cAAwC;CAChE,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,aAAa,EAAE,EAAE,UAAU,CAAC;CAC9F,OAAO;AACT;AAIA,SAAS,mBAAmB,GAAa,OAAqB,SAAwB;CACpF,MAAM,UAAU,UAAU;CAC1B,MAAM,SAAS,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;CACrC,MAAM,aAAa,KAAK,IAAI,GAAG,WAAW,OAAO,UAAU,EAAE;CAC7D,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK,OAAO,KAAK,IAAI;CAErD,IAAI,EAAE,iBAAiB,OAAO,QAAQ,UAAU,OAAO,cAAc,CAAC,CAAC;CACvE,OAAO;AACT;;AAGA,SAAS,cAAc,GAAsB;CAC3C,MAAM,SAAS,EAAE;CACjB,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,MAAM,IAAI,MAAM,8CAA8C,EAAE,UAAU;CAE5E,OAAO;AACT"}
|
package/build/serde/columns.cjs
CHANGED
|
@@ -35,6 +35,9 @@ var Column = class {
|
|
|
35
35
|
get isNullableArray() {
|
|
36
36
|
return this.codec.isArray && this.sqlNullable;
|
|
37
37
|
}
|
|
38
|
+
get notNullMarker() {
|
|
39
|
+
return this.codec.notNullMarker;
|
|
40
|
+
}
|
|
38
41
|
/** Resolve ID metadata only after all generated entity modules have initialized. */
|
|
39
42
|
get outputType() {
|
|
40
43
|
const output = this.codec.outputType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"columns.cjs","names":[],"sources":["../../src/serde/columns.ts"],"sourcesContent":["// These must be erased imports: descriptors are loaded before entity metadata and relation classes.\nimport type { EntityMetadata } from \"src/EntityMetadata.ts\";\nimport type { ScalarCodec } from \"src/serde/serde.ts\";\nimport type { TypeInfo } from \"src/serde/TypeInfo.ts\";\n\n/** A physical column owns one scalar codec, shared with every field binding for that column. */\nexport class Column {\n readonly insert: \"required\" | \"optional\" | \"never\";\n readonly update: boolean;\n /** Physical storage and SQL write policy, independent of domain field requiredness and defaults. */\n constructor(\n readonly columnName: string,\n readonly sqlNullable: boolean,\n readonly hasDefault: boolean,\n readonly isGenerated: boolean,\n /** ORM-maintained timestamps can be omitted by the SQL API as well. */\n readonly insertOptional: boolean,\n /** Polymorphic components are readable, but direct mutations remain unsupported. */\n readonly writable: boolean,\n readonly idMetadata: (() => EntityMetadata) | undefined,\n readonly codec: ScalarCodec,\n ) {\n const primaryKey = columnName === \"id\";\n this.insert =\n isGenerated || writable === false\n ? \"never\"\n : sqlNullable || hasDefault || insertOptional || (primaryKey && [\"int\", \"bigint\"].includes(codec.dbType))\n ? \"optional\"\n : \"required\";\n this.update = !primaryKey && !isGenerated && writable !== false;\n }\n\n get dbType(): string {\n return this.codec.dbType;\n }\n get isArray(): boolean {\n return this.codec.isArray;\n }\n get isNullableArray(): boolean {\n return this.codec.isArray && this.sqlNullable;\n }\n\n /** Resolve ID metadata only after all generated entity modules have initialized. */\n get outputType(): TypeInfo | undefined {\n const output = this.codec.outputType;\n if (!this.idMetadata) return output;\n // Scalar ID evidence must never be attached to an array domain or an unknown replacement codec.\n return output && !this.codec.isArray && !output.dbType.endsWith(\"[]\")\n ? { ...output, idMeta: this.idMetadata() }\n : undefined;\n }\n\n mapToDb(value: any): any {\n return this.codec.mapToDb(value);\n }\n mapToDbValue(value: unknown): unknown {\n return this.codec.mapToDbValue!(value);\n }\n mapFromDb(value: unknown): unknown {\n return this.codec.mapFromDb(value);\n }\n mapFromJsonAgg(value: any): any {\n return this.codec.mapFromJsonAgg(value);\n }\n}\n\nexport type ColumnDescriptors = Record<string, Column>;\n"],"mappings":";;;AAMA,IAAa,SAAb,MAAoB;CAKP;CACA;CACA;CACA;CAEA;CAEA;CACA;CACA;CAbX;CACA;;CAEA,YACE,YACA,aACA,YACA,aAEA,gBAEA,UACA,YACA,OACA;EAVS,KAAA,aAAA;EACA,KAAA,cAAA;EACA,KAAA,aAAA;EACA,KAAA,cAAA;EAEA,KAAA,iBAAA;EAEA,KAAA,WAAA;EACA,KAAA,aAAA;EACA,KAAA,QAAA;EAET,MAAM,aAAa,eAAe;EAClC,KAAK,SACH,eAAe,aAAa,QACxB,UACA,eAAe,cAAc,kBAAmB,cAAc,CAAC,OAAO,QAAQ,CAAC,CAAC,SAAS,MAAM,MAAM,IACnG,aACA;EACR,KAAK,SAAS,CAAC,cAAc,CAAC,eAAe,aAAa;CAC5D;CAEA,IAAI,SAAiB;EACnB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,UAAmB;EACrB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,kBAA2B;EAC7B,OAAO,KAAK,MAAM,WAAW,KAAK;CACpC;;CAGA,IAAI,aAAmC;EACrC,MAAM,SAAS,KAAK,MAAM;EAC1B,IAAI,CAAC,KAAK,YAAY,OAAO;EAE7B,OAAO,UAAU,CAAC,KAAK,MAAM,WAAW,CAAC,OAAO,OAAO,SAAS,IAAI,IAChE;GAAE,GAAG;GAAQ,QAAQ,KAAK,WAAW;EAAE,IACvC,KAAA;CACN;CAEA,QAAQ,OAAiB;EACvB,OAAO,KAAK,MAAM,QAAQ,KAAK;CACjC;CACA,aAAa,OAAyB;EACpC,OAAO,KAAK,MAAM,aAAc,KAAK;CACvC;CACA,UAAU,OAAyB;EACjC,OAAO,KAAK,MAAM,UAAU,KAAK;CACnC;CACA,eAAe,OAAiB;EAC9B,OAAO,KAAK,MAAM,eAAe,KAAK;CACxC;AACF"}
|
|
1
|
+
{"version":3,"file":"columns.cjs","names":[],"sources":["../../src/serde/columns.ts"],"sourcesContent":["// These must be erased imports: descriptors are loaded before entity metadata and relation classes.\nimport type { EntityMetadata } from \"src/EntityMetadata.ts\";\nimport type { ScalarCodec } from \"src/serde/serde.ts\";\nimport type { TypeInfo } from \"src/serde/TypeInfo.ts\";\n\n/** A physical column owns one scalar codec, shared with every field binding for that column. */\nexport class Column {\n readonly insert: \"required\" | \"optional\" | \"never\";\n readonly update: boolean;\n /** Physical storage and SQL write policy, independent of domain field requiredness and defaults. */\n constructor(\n readonly columnName: string,\n readonly sqlNullable: boolean,\n readonly hasDefault: boolean,\n readonly isGenerated: boolean,\n /** ORM-maintained timestamps can be omitted by the SQL API as well. */\n readonly insertOptional: boolean,\n /** Polymorphic components are readable, but direct mutations remain unsupported. */\n readonly writable: boolean,\n readonly idMetadata: (() => EntityMetadata) | undefined,\n readonly codec: ScalarCodec,\n ) {\n const primaryKey = columnName === \"id\";\n this.insert =\n isGenerated || writable === false\n ? \"never\"\n : sqlNullable || hasDefault || insertOptional || (primaryKey && [\"int\", \"bigint\"].includes(codec.dbType))\n ? \"optional\"\n : \"required\";\n this.update = !primaryKey && !isGenerated && writable !== false;\n }\n\n get dbType(): string {\n return this.codec.dbType;\n }\n get isArray(): boolean {\n return this.codec.isArray;\n }\n get isNullableArray(): boolean {\n return this.codec.isArray && this.sqlNullable;\n }\n get notNullMarker(): unknown {\n return this.codec.notNullMarker;\n }\n\n /** Resolve ID metadata only after all generated entity modules have initialized. */\n get outputType(): TypeInfo | undefined {\n const output = this.codec.outputType;\n if (!this.idMetadata) return output;\n // Scalar ID evidence must never be attached to an array domain or an unknown replacement codec.\n return output && !this.codec.isArray && !output.dbType.endsWith(\"[]\")\n ? { ...output, idMeta: this.idMetadata() }\n : undefined;\n }\n\n mapToDb(value: any): any {\n return this.codec.mapToDb(value);\n }\n mapToDbValue(value: unknown): unknown {\n return this.codec.mapToDbValue!(value);\n }\n mapFromDb(value: unknown): unknown {\n return this.codec.mapFromDb(value);\n }\n mapFromJsonAgg(value: any): any {\n return this.codec.mapFromJsonAgg(value);\n }\n}\n\nexport type ColumnDescriptors = Record<string, Column>;\n"],"mappings":";;;AAMA,IAAa,SAAb,MAAoB;CAKP;CACA;CACA;CACA;CAEA;CAEA;CACA;CACA;CAbX;CACA;;CAEA,YACE,YACA,aACA,YACA,aAEA,gBAEA,UACA,YACA,OACA;EAVS,KAAA,aAAA;EACA,KAAA,cAAA;EACA,KAAA,aAAA;EACA,KAAA,cAAA;EAEA,KAAA,iBAAA;EAEA,KAAA,WAAA;EACA,KAAA,aAAA;EACA,KAAA,QAAA;EAET,MAAM,aAAa,eAAe;EAClC,KAAK,SACH,eAAe,aAAa,QACxB,UACA,eAAe,cAAc,kBAAmB,cAAc,CAAC,OAAO,QAAQ,CAAC,CAAC,SAAS,MAAM,MAAM,IACnG,aACA;EACR,KAAK,SAAS,CAAC,cAAc,CAAC,eAAe,aAAa;CAC5D;CAEA,IAAI,SAAiB;EACnB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,UAAmB;EACrB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,kBAA2B;EAC7B,OAAO,KAAK,MAAM,WAAW,KAAK;CACpC;CACA,IAAI,gBAAyB;EAC3B,OAAO,KAAK,MAAM;CACpB;;CAGA,IAAI,aAAmC;EACrC,MAAM,SAAS,KAAK,MAAM;EAC1B,IAAI,CAAC,KAAK,YAAY,OAAO;EAE7B,OAAO,UAAU,CAAC,KAAK,MAAM,WAAW,CAAC,OAAO,OAAO,SAAS,IAAI,IAChE;GAAE,GAAG;GAAQ,QAAQ,KAAK,WAAW;EAAE,IACvC,KAAA;CACN;CAEA,QAAQ,OAAiB;EACvB,OAAO,KAAK,MAAM,QAAQ,KAAK;CACjC;CACA,aAAa,OAAyB;EACpC,OAAO,KAAK,MAAM,aAAc,KAAK;CACvC;CACA,UAAU,OAAyB;EACjC,OAAO,KAAK,MAAM,UAAU,KAAK;CACnC;CACA,eAAe,OAAiB;EAC9B,OAAO,KAAK,MAAM,eAAe,KAAK;CACxC;AACF"}
|
|
@@ -25,6 +25,7 @@ export declare class Column {
|
|
|
25
25
|
get dbType(): string;
|
|
26
26
|
get isArray(): boolean;
|
|
27
27
|
get isNullableArray(): boolean;
|
|
28
|
+
get notNullMarker(): unknown;
|
|
28
29
|
/** Resolve ID metadata only after all generated entity modules have initialized. */
|
|
29
30
|
get outputType(): TypeInfo | undefined;
|
|
30
31
|
mapToDb(value: any): any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"columns.d.cts","names":[],"sources":["../../src/serde/columns.ts"],"mappings":";;;;;qBAMa;WAKA;WACA;WACA;WACA;;WAEA;;WAEA;WACA,mBAAmB;WACnB,OAAO;WAbT;WACA;;EAET,YACW,oBACA,sBACA,qBACA;;EAEA;;EAEA,mBACA,mBAAmB,6BACnB,OAAO;MAYd;MAGA;MAGA;;MAKA,cAAc;EASlB,QAAQ;EAGR,aAAa;EAGb,UAAU;EAGV,eAAe;;YAKL,oBAAoB,eAAe"}
|
|
1
|
+
{"version":3,"file":"columns.d.cts","names":[],"sources":["../../src/serde/columns.ts"],"mappings":";;;;;qBAMa;WAKA;WACA;WACA;WACA;;WAEA;;WAEA;WACA,mBAAmB;WACnB,OAAO;WAbT;WACA;;EAET,YACW,oBACA,sBACA,qBACA;;EAEA;;EAEA,mBACA,mBAAmB,6BACnB,OAAO;MAYd;MAGA;MAGA;MAGA;;MAKA,cAAc;EASlB,QAAQ;EAGR,aAAa;EAGb,UAAU;EAGV,eAAe;;YAKL,oBAAoB,eAAe"}
|
|
@@ -25,6 +25,7 @@ export declare class Column {
|
|
|
25
25
|
get dbType(): string;
|
|
26
26
|
get isArray(): boolean;
|
|
27
27
|
get isNullableArray(): boolean;
|
|
28
|
+
get notNullMarker(): unknown;
|
|
28
29
|
/** Resolve ID metadata only after all generated entity modules have initialized. */
|
|
29
30
|
get outputType(): TypeInfo | undefined;
|
|
30
31
|
mapToDb(value: any): any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"columns.d.mts","names":[],"sources":["../../src/serde/columns.ts"],"mappings":";;;;;qBAMa;WAKA;WACA;WACA;WACA;;WAEA;;WAEA;WACA,mBAAmB;WACnB,OAAO;WAbT;WACA;;EAET,YACW,oBACA,sBACA,qBACA;;EAEA;;EAEA,mBACA,mBAAmB,6BACnB,OAAO;MAYd;MAGA;MAGA;;MAKA,cAAc;EASlB,QAAQ;EAGR,aAAa;EAGb,UAAU;EAGV,eAAe;;YAKL,oBAAoB,eAAe"}
|
|
1
|
+
{"version":3,"file":"columns.d.mts","names":[],"sources":["../../src/serde/columns.ts"],"mappings":";;;;;qBAMa;WAKA;WACA;WACA;WACA;;WAEA;;WAEA;WACA,mBAAmB;WACnB,OAAO;WAbT;WACA;;EAET,YACW,oBACA,sBACA,qBACA;;EAEA;;EAEA,mBACA,mBAAmB,6BACnB,OAAO;MAYd;MAGA;MAGA;MAGA;;MAKA,cAAc;EASlB,QAAQ;EAGR,aAAa;EAGb,UAAU;EAGV,eAAe;;YAKL,oBAAoB,eAAe"}
|
package/build/serde/columns.js
CHANGED
|
@@ -34,6 +34,9 @@ var Column = class {
|
|
|
34
34
|
get isNullableArray() {
|
|
35
35
|
return this.codec.isArray && this.sqlNullable;
|
|
36
36
|
}
|
|
37
|
+
get notNullMarker() {
|
|
38
|
+
return this.codec.notNullMarker;
|
|
39
|
+
}
|
|
37
40
|
/** Resolve ID metadata only after all generated entity modules have initialized. */
|
|
38
41
|
get outputType() {
|
|
39
42
|
const output = this.codec.outputType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"columns.js","names":[],"sources":["../../src/serde/columns.ts"],"sourcesContent":["// These must be erased imports: descriptors are loaded before entity metadata and relation classes.\nimport type { EntityMetadata } from \"src/EntityMetadata.ts\";\nimport type { ScalarCodec } from \"src/serde/serde.ts\";\nimport type { TypeInfo } from \"src/serde/TypeInfo.ts\";\n\n/** A physical column owns one scalar codec, shared with every field binding for that column. */\nexport class Column {\n readonly insert: \"required\" | \"optional\" | \"never\";\n readonly update: boolean;\n /** Physical storage and SQL write policy, independent of domain field requiredness and defaults. */\n constructor(\n readonly columnName: string,\n readonly sqlNullable: boolean,\n readonly hasDefault: boolean,\n readonly isGenerated: boolean,\n /** ORM-maintained timestamps can be omitted by the SQL API as well. */\n readonly insertOptional: boolean,\n /** Polymorphic components are readable, but direct mutations remain unsupported. */\n readonly writable: boolean,\n readonly idMetadata: (() => EntityMetadata) | undefined,\n readonly codec: ScalarCodec,\n ) {\n const primaryKey = columnName === \"id\";\n this.insert =\n isGenerated || writable === false\n ? \"never\"\n : sqlNullable || hasDefault || insertOptional || (primaryKey && [\"int\", \"bigint\"].includes(codec.dbType))\n ? \"optional\"\n : \"required\";\n this.update = !primaryKey && !isGenerated && writable !== false;\n }\n\n get dbType(): string {\n return this.codec.dbType;\n }\n get isArray(): boolean {\n return this.codec.isArray;\n }\n get isNullableArray(): boolean {\n return this.codec.isArray && this.sqlNullable;\n }\n\n /** Resolve ID metadata only after all generated entity modules have initialized. */\n get outputType(): TypeInfo | undefined {\n const output = this.codec.outputType;\n if (!this.idMetadata) return output;\n // Scalar ID evidence must never be attached to an array domain or an unknown replacement codec.\n return output && !this.codec.isArray && !output.dbType.endsWith(\"[]\")\n ? { ...output, idMeta: this.idMetadata() }\n : undefined;\n }\n\n mapToDb(value: any): any {\n return this.codec.mapToDb(value);\n }\n mapToDbValue(value: unknown): unknown {\n return this.codec.mapToDbValue!(value);\n }\n mapFromDb(value: unknown): unknown {\n return this.codec.mapFromDb(value);\n }\n mapFromJsonAgg(value: any): any {\n return this.codec.mapFromJsonAgg(value);\n }\n}\n\nexport type ColumnDescriptors = Record<string, Column>;\n"],"mappings":";;AAMA,IAAa,SAAb,MAAoB;CAKP;CACA;CACA;CACA;CAEA;CAEA;CACA;CACA;CAbX;CACA;;CAEA,YACE,YACA,aACA,YACA,aAEA,gBAEA,UACA,YACA,OACA;EAVS,KAAA,aAAA;EACA,KAAA,cAAA;EACA,KAAA,aAAA;EACA,KAAA,cAAA;EAEA,KAAA,iBAAA;EAEA,KAAA,WAAA;EACA,KAAA,aAAA;EACA,KAAA,QAAA;EAET,MAAM,aAAa,eAAe;EAClC,KAAK,SACH,eAAe,aAAa,QACxB,UACA,eAAe,cAAc,kBAAmB,cAAc,CAAC,OAAO,QAAQ,CAAC,CAAC,SAAS,MAAM,MAAM,IACnG,aACA;EACR,KAAK,SAAS,CAAC,cAAc,CAAC,eAAe,aAAa;CAC5D;CAEA,IAAI,SAAiB;EACnB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,UAAmB;EACrB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,kBAA2B;EAC7B,OAAO,KAAK,MAAM,WAAW,KAAK;CACpC;;CAGA,IAAI,aAAmC;EACrC,MAAM,SAAS,KAAK,MAAM;EAC1B,IAAI,CAAC,KAAK,YAAY,OAAO;EAE7B,OAAO,UAAU,CAAC,KAAK,MAAM,WAAW,CAAC,OAAO,OAAO,SAAS,IAAI,IAChE;GAAE,GAAG;GAAQ,QAAQ,KAAK,WAAW;EAAE,IACvC,KAAA;CACN;CAEA,QAAQ,OAAiB;EACvB,OAAO,KAAK,MAAM,QAAQ,KAAK;CACjC;CACA,aAAa,OAAyB;EACpC,OAAO,KAAK,MAAM,aAAc,KAAK;CACvC;CACA,UAAU,OAAyB;EACjC,OAAO,KAAK,MAAM,UAAU,KAAK;CACnC;CACA,eAAe,OAAiB;EAC9B,OAAO,KAAK,MAAM,eAAe,KAAK;CACxC;AACF"}
|
|
1
|
+
{"version":3,"file":"columns.js","names":[],"sources":["../../src/serde/columns.ts"],"sourcesContent":["// These must be erased imports: descriptors are loaded before entity metadata and relation classes.\nimport type { EntityMetadata } from \"src/EntityMetadata.ts\";\nimport type { ScalarCodec } from \"src/serde/serde.ts\";\nimport type { TypeInfo } from \"src/serde/TypeInfo.ts\";\n\n/** A physical column owns one scalar codec, shared with every field binding for that column. */\nexport class Column {\n readonly insert: \"required\" | \"optional\" | \"never\";\n readonly update: boolean;\n /** Physical storage and SQL write policy, independent of domain field requiredness and defaults. */\n constructor(\n readonly columnName: string,\n readonly sqlNullable: boolean,\n readonly hasDefault: boolean,\n readonly isGenerated: boolean,\n /** ORM-maintained timestamps can be omitted by the SQL API as well. */\n readonly insertOptional: boolean,\n /** Polymorphic components are readable, but direct mutations remain unsupported. */\n readonly writable: boolean,\n readonly idMetadata: (() => EntityMetadata) | undefined,\n readonly codec: ScalarCodec,\n ) {\n const primaryKey = columnName === \"id\";\n this.insert =\n isGenerated || writable === false\n ? \"never\"\n : sqlNullable || hasDefault || insertOptional || (primaryKey && [\"int\", \"bigint\"].includes(codec.dbType))\n ? \"optional\"\n : \"required\";\n this.update = !primaryKey && !isGenerated && writable !== false;\n }\n\n get dbType(): string {\n return this.codec.dbType;\n }\n get isArray(): boolean {\n return this.codec.isArray;\n }\n get isNullableArray(): boolean {\n return this.codec.isArray && this.sqlNullable;\n }\n get notNullMarker(): unknown {\n return this.codec.notNullMarker;\n }\n\n /** Resolve ID metadata only after all generated entity modules have initialized. */\n get outputType(): TypeInfo | undefined {\n const output = this.codec.outputType;\n if (!this.idMetadata) return output;\n // Scalar ID evidence must never be attached to an array domain or an unknown replacement codec.\n return output && !this.codec.isArray && !output.dbType.endsWith(\"[]\")\n ? { ...output, idMeta: this.idMetadata() }\n : undefined;\n }\n\n mapToDb(value: any): any {\n return this.codec.mapToDb(value);\n }\n mapToDbValue(value: unknown): unknown {\n return this.codec.mapToDbValue!(value);\n }\n mapFromDb(value: unknown): unknown {\n return this.codec.mapFromDb(value);\n }\n mapFromJsonAgg(value: any): any {\n return this.codec.mapFromJsonAgg(value);\n }\n}\n\nexport type ColumnDescriptors = Record<string, Column>;\n"],"mappings":";;AAMA,IAAa,SAAb,MAAoB;CAKP;CACA;CACA;CACA;CAEA;CAEA;CACA;CACA;CAbX;CACA;;CAEA,YACE,YACA,aACA,YACA,aAEA,gBAEA,UACA,YACA,OACA;EAVS,KAAA,aAAA;EACA,KAAA,cAAA;EACA,KAAA,aAAA;EACA,KAAA,cAAA;EAEA,KAAA,iBAAA;EAEA,KAAA,WAAA;EACA,KAAA,aAAA;EACA,KAAA,QAAA;EAET,MAAM,aAAa,eAAe;EAClC,KAAK,SACH,eAAe,aAAa,QACxB,UACA,eAAe,cAAc,kBAAmB,cAAc,CAAC,OAAO,QAAQ,CAAC,CAAC,SAAS,MAAM,MAAM,IACnG,aACA;EACR,KAAK,SAAS,CAAC,cAAc,CAAC,eAAe,aAAa;CAC5D;CAEA,IAAI,SAAiB;EACnB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,UAAmB;EACrB,OAAO,KAAK,MAAM;CACpB;CACA,IAAI,kBAA2B;EAC7B,OAAO,KAAK,MAAM,WAAW,KAAK;CACpC;CACA,IAAI,gBAAyB;EAC3B,OAAO,KAAK,MAAM;CACpB;;CAGA,IAAI,aAAmC;EACrC,MAAM,SAAS,KAAK,MAAM;EAC1B,IAAI,CAAC,KAAK,YAAY,OAAO;EAE7B,OAAO,UAAU,CAAC,KAAK,MAAM,WAAW,CAAC,OAAO,OAAO,SAAS,IAAI,IAChE;GAAE,GAAG;GAAQ,QAAQ,KAAK,WAAW;EAAE,IACvC,KAAA;CACN;CAEA,QAAQ,OAAiB;EACvB,OAAO,KAAK,MAAM,QAAQ,KAAK;CACjC;CACA,aAAa,OAAyB;EACpC,OAAO,KAAK,MAAM,aAAc,KAAK;CACvC;CACA,UAAU,OAAyB;EACjC,OAAO,KAAK,MAAM,UAAU,KAAK;CACnC;CACA,eAAe,OAAiB;EAC9B,OAAO,KAAK,MAAM,eAAe,KAAK;CACxC;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fieldSerde.cjs","names":["isEntity","getMetadata","maybeResolveReferenceToId","getConstructorFromTaggedId","groupBy","getBaseMeta"],"sources":["../../src/serde/fieldSerde.ts"],"sourcesContent":["import { getConstructorFromTaggedId } from \"src/configure.ts\";\nimport type { InsertFixup } from \"src/drivers/EntityWriter.ts\";\nimport { type Entity, isEntity } from \"src/Entity.ts\";\nimport {\n type EntityMetadata,\n type Field,\n type PolymorphicField,\n type PolymorphicFieldComponent,\n type SerdeField,\n getBaseMeta,\n getMetadata,\n} from \"src/EntityMetadata.ts\";\nimport { maybeResolveReferenceToId } from \"src/keys.ts\";\nimport type { RowData } from \"src/RowData.ts\";\nimport type { Column } from \"src/serde/columns.ts\";\nimport type { TimestampCodec } from \"src/serde/serde.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\nexport function hasSerde(field: Field): field is SerdeField {\n return !!field.serde;\n}\n\n/** Entity hydration and flush bindings; a polymorphic relationship spans several physical columns. */\nexport interface FieldSerde {\n columns: FieldColumn[];\n /** Decode one domain value from accessed cells; callers own entity assignment and caching. */\n fromRow(rowData: RowData, rowIndex: number): unknown;\n}\n\nexport interface TimestampSerde<T> extends FieldSerde {\n mapFromNow(now: Date): T;\n dbValue(data: any): any;\n}\n\n/** Binds a physical column to entity data without adding a second codec or storage declaration. */\nexport class FieldColumn {\n constructor(\n readonly fieldName: string,\n readonly column: Column,\n ) {}\n get codec() {\n return this.column.codec;\n }\n get insertOptional() {\n return this.column.insertOptional;\n }\n get writable() {\n return this.column.writable;\n }\n get insert() {\n return this.column.insert;\n }\n get update() {\n return this.column.update;\n }\n get idMetadata() {\n return this.column.idMetadata;\n }\n get columnName(): string {\n return this.column.columnName;\n }\n get dbType(): string {\n return this.column.dbType;\n }\n get isArray(): boolean {\n return this.column.isArray;\n }\n get isNullableArray(): boolean {\n return this.column.isNullableArray;\n }\n get sqlNullable(): boolean {\n return this.column.sqlNullable;\n }\n get hasDefault(): boolean {\n return this.column.hasDefault;\n }\n get isGenerated(): boolean {\n return this.column.isGenerated;\n }\n get outputType() {\n return this.column.outputType;\n }\n mapToDb(value: any): any {\n return this.column.mapToDb(value);\n }\n mapToDbValue(value: unknown): unknown {\n return this.column.mapToDbValue(value);\n }\n mapFromDb(value: unknown): unknown {\n return this.column.mapFromDb(value);\n }\n mapFromJsonAgg(value: any): any {\n return this.column.mapFromJsonAgg(value);\n }\n\n /** Defers FK writes whose new target must be inserted after this entity. */\n dbValue(data: any, entity: Entity, tableName: string, fixups: InsertFixup[] | undefined): any {\n const value = data[this.fieldName];\n if (\n this.column.idMetadata &&\n fixups &&\n isEntity(value) &&\n value.isNewEntity &&\n getMetadata(value).nonDeferredFkOrder &&\n getMetadata(entity).nonDeferredFkOrder &&\n getMetadata(value).nonDeferredFkOrder! >= getMetadata(entity).nonDeferredFkOrder!\n ) {\n fixups.push({ entity, tableName, column: this, value: this.mapToDbValue(maybeResolveReferenceToId(value)) });\n return null;\n }\n return this.mapToDbValue(value);\n }\n\n /** Reconstruct the driver's row representation, which can differ from write bindings for Date and JSON. */\n rowValue(data: any): any {\n return this.column.codec.mapToRow(data[this.fieldName]);\n }\n}\n\n/** Binds one entity field to its physical column. */\nexport class SimpleFieldSerde extends FieldColumn implements FieldSerde {\n readonly columns: FieldColumn[] = [this];\n /** Decode this field's physical column without changing entity data. */\n fromRow(rowData: RowData, rowIndex: number): unknown {\n return this.column.mapFromDb(rowData.get(rowIndex, this.column.columnName));\n }\n mapFromNow(now: Date): unknown {\n return (this.column.codec as TimestampCodec<unknown>).mapFromNow(now);\n }\n}\n\n/** A polymorphic field selects exactly one component while each physical column retains a scalar ID codec. */\nexport class PolymorphicKeySerde implements FieldSerde {\n readonly columns: PolymorphicFieldColumn[];\n constructor(\n readonly fieldName: string,\n components: PolymorphicFieldComponent[],\n ) {\n this.columns = components.map((component) => new PolymorphicFieldColumn(fieldName, component, components));\n }\n /** Select the first truthy component whose decoded ID is not nullish. */\n fromRow(rowData: RowData, rowIndex: number): unknown {\n let decoded: unknown;\n for (const column of this.columns) {\n const value = rowData.get(rowIndex, column.columnName);\n if (value) {\n decoded = column.mapFromDb(value);\n if (decoded != null) return decoded;\n }\n }\n return decoded;\n }\n}\n\n/** Shares domain components between relationship traversal and entity write selection. */\nexport function polymorphicField(\n fieldName: string,\n required: boolean,\n components: PolymorphicFieldComponent[],\n): PolymorphicField {\n return {\n kind: \"poly\",\n fieldName,\n fieldIdName: `${fieldName}Id`,\n required,\n components,\n serde: new PolymorphicKeySerde(fieldName, components),\n immutable: false,\n };\n}\n\n/** A domain relationship component references storage and only overrides a specialized domain target. */\nexport class PolyComponent implements PolymorphicFieldComponent {\n constructor(\n readonly column: Column,\n readonly otherFieldName: string,\n private readonly target?: () => EntityMetadata,\n ) {}\n get columnName(): string {\n return this.column.columnName;\n }\n get otherMetadata(): () => EntityMetadata {\n return this.target ?? this.column.idMetadata!;\n }\n}\n\n/** Selects a relationship component for entity writes only; direct SQL writes use its scalar column codec. */\nclass PolymorphicFieldColumn extends FieldColumn {\n constructor(\n fieldName: string,\n private readonly component: PolymorphicFieldComponent,\n private readonly components: PolymorphicFieldComponent[],\n ) {\n super(fieldName, component.column);\n }\n get otherMetadata() {\n return this.component.otherMetadata;\n }\n\n /** Preserve exact subtype selection when multiple components share a base type. */\n mapToDbValue(value: unknown): unknown {\n const id = maybeResolveReferenceToId(value as Parameters<typeof maybeResolveReferenceToId>[0]);\n const cstr = isEntity(value) ? getMetadata(value).cstr : id ? getConstructorFromTaggedId(id) : undefined;\n // If our poly has multiple components from the same base type, i.e.\n // `parent_small_publisher_id` and `parent_large_publisher_id`, then we\n // need slightly different logic...\n const hasMultipleComponentsWithSameBaseType = [\n ...groupBy(this.components, (component) => getBaseMeta(component.otherMetadata()).type).values(),\n ].some((group) => group.length > 1);\n const otherMeta = this.otherMetadata();\n // We'll have multiple columns, i.e. [parent_author_id, parent_book_id], and each column\n // will only return a value if the `id` matches its type, i.e. `parent_author_id=a:1` will\n // return 1, but `parent_book_id` will return null.\n const applies = hasMultipleComponentsWithSameBaseType\n ? cstr === otherMeta.cstr\n : cstr === otherMeta.cstr ||\n cstr === getBaseMeta(otherMeta).cstr ||\n otherMeta.subTypes.some((subTypeMeta) => cstr === subTypeMeta.cstr);\n return applies ? this.column.mapToDbValue(id) : undefined;\n }\n // Polymorphic writes historically do not schedule non-deferred FK fixups.\n dbValue(data: any): any {\n return this.mapToDbValue(data[this.fieldName]);\n }\n rowValue(data: any): any {\n return this.dbValue(data);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fieldSerde.cjs","names":["isEntity","getMetadata","maybeResolveReferenceToId","getConstructorFromTaggedId","groupBy","getBaseMeta"],"sources":["../../src/serde/fieldSerde.ts"],"sourcesContent":["import { getConstructorFromTaggedId } from \"src/configure.ts\";\nimport type { InsertFixup } from \"src/drivers/EntityWriter.ts\";\nimport { type Entity, isEntity } from \"src/Entity.ts\";\nimport {\n type EntityMetadata,\n type Field,\n type PolymorphicField,\n type PolymorphicFieldComponent,\n type SerdeField,\n getBaseMeta,\n getMetadata,\n} from \"src/EntityMetadata.ts\";\nimport { maybeResolveReferenceToId } from \"src/keys.ts\";\nimport type { RowData } from \"src/RowData.ts\";\nimport type { Column } from \"src/serde/columns.ts\";\nimport type { TimestampCodec } from \"src/serde/serde.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\nexport function hasSerde(field: Field): field is SerdeField {\n return !!field.serde;\n}\n\n/** Entity hydration and flush bindings; a polymorphic relationship spans several physical columns. */\nexport interface FieldSerde {\n columns: FieldColumn[];\n /** Decode one domain value from accessed cells; callers own entity assignment and caching. */\n fromRow(rowData: RowData, rowIndex: number): unknown;\n}\n\nexport interface TimestampSerde<T> extends FieldSerde {\n mapFromNow(now: Date): T;\n dbValue(data: any): any;\n}\n\n/** Binds a physical column to entity data without adding a second codec or storage declaration. */\nexport class FieldColumn {\n constructor(\n readonly fieldName: string,\n readonly column: Column,\n ) {}\n get codec() {\n return this.column.codec;\n }\n get insertOptional() {\n return this.column.insertOptional;\n }\n get writable() {\n return this.column.writable;\n }\n get insert() {\n return this.column.insert;\n }\n get update() {\n return this.column.update;\n }\n get idMetadata() {\n return this.column.idMetadata;\n }\n get columnName(): string {\n return this.column.columnName;\n }\n get dbType(): string {\n return this.column.dbType;\n }\n get isArray(): boolean {\n return this.column.isArray;\n }\n get isNullableArray(): boolean {\n return this.column.isNullableArray;\n }\n get notNullMarker(): unknown {\n return this.column.notNullMarker;\n }\n get sqlNullable(): boolean {\n return this.column.sqlNullable;\n }\n get hasDefault(): boolean {\n return this.column.hasDefault;\n }\n get isGenerated(): boolean {\n return this.column.isGenerated;\n }\n get outputType() {\n return this.column.outputType;\n }\n mapToDb(value: any): any {\n return this.column.mapToDb(value);\n }\n mapToDbValue(value: unknown): unknown {\n return this.column.mapToDbValue(value);\n }\n mapFromDb(value: unknown): unknown {\n return this.column.mapFromDb(value);\n }\n mapFromJsonAgg(value: any): any {\n return this.column.mapFromJsonAgg(value);\n }\n\n /** Defers FK writes whose new target must be inserted after this entity. */\n dbValue(data: any, entity: Entity, tableName: string, fixups: InsertFixup[] | undefined): any {\n const value = data[this.fieldName];\n if (\n this.column.idMetadata &&\n fixups &&\n isEntity(value) &&\n value.isNewEntity &&\n getMetadata(value).nonDeferredFkOrder &&\n getMetadata(entity).nonDeferredFkOrder &&\n getMetadata(value).nonDeferredFkOrder! >= getMetadata(entity).nonDeferredFkOrder!\n ) {\n fixups.push({ entity, tableName, column: this, value: this.mapToDbValue(maybeResolveReferenceToId(value)) });\n return null;\n }\n return this.mapToDbValue(value);\n }\n\n /** Reconstruct the driver's row representation, which can differ from write bindings for Date and JSON. */\n rowValue(data: any): any {\n return this.column.codec.mapToRow(data[this.fieldName]);\n }\n}\n\n/** Binds one entity field to its physical column. */\nexport class SimpleFieldSerde extends FieldColumn implements FieldSerde {\n readonly columns: FieldColumn[] = [this];\n /** Decode this field's physical column without changing entity data. */\n fromRow(rowData: RowData, rowIndex: number): unknown {\n return this.column.mapFromDb(rowData.get(rowIndex, this.column.columnName));\n }\n mapFromNow(now: Date): unknown {\n return (this.column.codec as TimestampCodec<unknown>).mapFromNow(now);\n }\n}\n\n/** A polymorphic field selects exactly one component while each physical column retains a scalar ID codec. */\nexport class PolymorphicKeySerde implements FieldSerde {\n readonly columns: PolymorphicFieldColumn[];\n constructor(\n readonly fieldName: string,\n components: PolymorphicFieldComponent[],\n ) {\n this.columns = components.map((component) => new PolymorphicFieldColumn(fieldName, component, components));\n }\n /** Select the first truthy component whose decoded ID is not nullish. */\n fromRow(rowData: RowData, rowIndex: number): unknown {\n let decoded: unknown;\n for (const column of this.columns) {\n const value = rowData.get(rowIndex, column.columnName);\n if (value) {\n decoded = column.mapFromDb(value);\n if (decoded != null) return decoded;\n }\n }\n return decoded;\n }\n}\n\n/** Shares domain components between relationship traversal and entity write selection. */\nexport function polymorphicField(\n fieldName: string,\n required: boolean,\n components: PolymorphicFieldComponent[],\n): PolymorphicField {\n return {\n kind: \"poly\",\n fieldName,\n fieldIdName: `${fieldName}Id`,\n required,\n components,\n serde: new PolymorphicKeySerde(fieldName, components),\n immutable: false,\n };\n}\n\n/** A domain relationship component references storage and only overrides a specialized domain target. */\nexport class PolyComponent implements PolymorphicFieldComponent {\n constructor(\n readonly column: Column,\n readonly otherFieldName: string,\n private readonly target?: () => EntityMetadata,\n ) {}\n get columnName(): string {\n return this.column.columnName;\n }\n get otherMetadata(): () => EntityMetadata {\n return this.target ?? this.column.idMetadata!;\n }\n}\n\n/** Selects a relationship component for entity writes only; direct SQL writes use its scalar column codec. */\nclass PolymorphicFieldColumn extends FieldColumn {\n constructor(\n fieldName: string,\n private readonly component: PolymorphicFieldComponent,\n private readonly components: PolymorphicFieldComponent[],\n ) {\n super(fieldName, component.column);\n }\n get otherMetadata() {\n return this.component.otherMetadata;\n }\n\n /** Preserve exact subtype selection when multiple components share a base type. */\n mapToDbValue(value: unknown): unknown {\n const id = maybeResolveReferenceToId(value as Parameters<typeof maybeResolveReferenceToId>[0]);\n const cstr = isEntity(value) ? getMetadata(value).cstr : id ? getConstructorFromTaggedId(id) : undefined;\n // If our poly has multiple components from the same base type, i.e.\n // `parent_small_publisher_id` and `parent_large_publisher_id`, then we\n // need slightly different logic...\n const hasMultipleComponentsWithSameBaseType = [\n ...groupBy(this.components, (component) => getBaseMeta(component.otherMetadata()).type).values(),\n ].some((group) => group.length > 1);\n const otherMeta = this.otherMetadata();\n // We'll have multiple columns, i.e. [parent_author_id, parent_book_id], and each column\n // will only return a value if the `id` matches its type, i.e. `parent_author_id=a:1` will\n // return 1, but `parent_book_id` will return null.\n const applies = hasMultipleComponentsWithSameBaseType\n ? cstr === otherMeta.cstr\n : cstr === otherMeta.cstr ||\n cstr === getBaseMeta(otherMeta).cstr ||\n otherMeta.subTypes.some((subTypeMeta) => cstr === subTypeMeta.cstr);\n return applies ? this.column.mapToDbValue(id) : undefined;\n }\n // Polymorphic writes historically do not schedule non-deferred FK fixups.\n dbValue(data: any): any {\n return this.mapToDbValue(data[this.fieldName]);\n }\n rowValue(data: any): any {\n return this.dbValue(data);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fieldSerde.d.cts","names":["_$src_serde_serde_ts0.ScalarCodec","_$__TypeInfo_ts0.TypeInfo"],"sources":["../../src/serde/fieldSerde.ts"],"mappings":";;;;;;;;wBAkBgB,SAAS,OAAO,QAAQ,SAAS;;iBAKhC;EACf,SAAS;;EAET,QAAQ,SAAS,SAAS;;iBAGX,eAAe,WAAW;EACzC,WAAW,KAAK,OAAO;EACvB,QAAQ;;;qBAIG;WAEA;WACA,QAAQ;EAFnB,YACW,mBACA,QAAQ;MAEf,SAFeA;MAKf;MAGA;MAGA;MAGA;MAGA,qBAAU;MAGV;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA,
|
|
1
|
+
{"version":3,"file":"fieldSerde.d.cts","names":["_$src_serde_serde_ts0.ScalarCodec","_$__TypeInfo_ts0.TypeInfo"],"sources":["../../src/serde/fieldSerde.ts"],"mappings":";;;;;;;;wBAkBgB,SAAS,OAAO,QAAQ,SAAS;;iBAKhC;EACf,SAAS;;EAET,QAAQ,SAAS,SAAS;;iBAGX,eAAe,WAAW;EACzC,WAAW,KAAK,OAAO;EACvB,QAAQ;;;qBAIG;WAEA;WACA,QAAQ;EAFnB,YACW,mBACA,QAAQ;MAEf,SAFeA;MAKf;MAGA;MAGA;MAGA;MAGA,qBAAU;MAGV;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA,cA3BUC;EA8Bd,QAAQ;EAGR,aAAa;EAGb,UAAU;EAGV,eAAe;;EAKf,QAAQ,WAAW,QAAQ,QAAQ,mBAAmB,QAAQ;;EAkB9D,SAAS;;;qBAME,yBAAyB,uBAAuB;WAClD,SAAS;;EAElB,QAAQ,SAAS,SAAS;EAG1B,WAAW,KAAK;;;qBAML,+BAA+B;WAG/B;WAFF,SAAS;EAClB,YACW,mBACT,YAAY;;EAKd,QAAQ,SAAS,SAAS;;;wBAcZ,iBACd,mBACA,mBACA,YAAY,8BACX;;qBAaU,yBAAyB;WAEzB,QAAQ;WACR;mBACQ;EAHnB,YACW,QAAQ,QACR,wBACQ,gBAAe;MAE9B;MAGA,uBAAuB;;;cAMvB,+BAA+B;mBAGhB;mBACA;EAHnB,YACE,mBACiB,WAAW,2BACX,YAAY;MAI3B,uBAAa;;EAKjB,aAAa;EAqBb,QAAQ;EAGR,SAAS"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fieldSerde.d.mts","names":["_$src_serde_serde_ts0.ScalarCodec","_$__TypeInfo_ts0.TypeInfo"],"sources":["../../src/serde/fieldSerde.ts"],"mappings":";;;;;;;;wBAkBgB,SAAS,OAAO,QAAQ,SAAS;;iBAKhC;EACf,SAAS;;EAET,QAAQ,SAAS,SAAS;;iBAGX,eAAe,WAAW;EACzC,WAAW,KAAK,OAAO;EACvB,QAAQ;;;qBAIG;WAEA;WACA,QAAQ;EAFnB,YACW,mBACA,QAAQ;MAEf,SAFeA;MAKf;MAGA;MAGA;MAGA;MAGA,qBAAU;MAGV;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA,
|
|
1
|
+
{"version":3,"file":"fieldSerde.d.mts","names":["_$src_serde_serde_ts0.ScalarCodec","_$__TypeInfo_ts0.TypeInfo"],"sources":["../../src/serde/fieldSerde.ts"],"mappings":";;;;;;;;wBAkBgB,SAAS,OAAO,QAAQ,SAAS;;iBAKhC;EACf,SAAS;;EAET,QAAQ,SAAS,SAAS;;iBAGX,eAAe,WAAW;EACzC,WAAW,KAAK,OAAO;EACvB,QAAQ;;;qBAIG;WAEA;WACA,QAAQ;EAFnB,YACW,mBACA,QAAQ;MAEf,SAFeA;MAKf;MAGA;MAGA;MAGA;MAGA,qBAAU;MAGV;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA;MAGA,cA3BUC;EA8Bd,QAAQ;EAGR,aAAa;EAGb,UAAU;EAGV,eAAe;;EAKf,QAAQ,WAAW,QAAQ,QAAQ,mBAAmB,QAAQ;;EAkB9D,SAAS;;;qBAME,yBAAyB,uBAAuB;WAClD,SAAS;;EAElB,QAAQ,SAAS,SAAS;EAG1B,WAAW,KAAK;;;qBAML,+BAA+B;WAG/B;WAFF,SAAS;EAClB,YACW,mBACT,YAAY;;EAKd,QAAQ,SAAS,SAAS;;;wBAcZ,iBACd,mBACA,mBACA,YAAY,8BACX;;qBAaU,yBAAyB;WAEzB,QAAQ;WACR;mBACQ;EAHnB,YACW,QAAQ,QACR,wBACQ,gBAAe;MAE9B;MAGA,uBAAuB;;;cAMvB,+BAA+B;mBAGhB;mBACA;EAHnB,YACE,mBACiB,WAAW,2BACX,YAAY;MAI3B,uBAAa;;EAKjB,aAAa;EAqBb,QAAQ;EAGR,SAAS"}
|