I'm not sure my experiences answer your question, but here's what we do in case helpful.
We have a Stored Procedure for UPDATE. It has a parameter for Must exist, Must not exist or Don't care. So if an operator is using a CREATE process then the record must be new; if using a MODIFY process then the account must exist; if doing some sort of import maybe you want an UpSert, in which case you don't care.
Our applications are Web based, so data is coming from Web Forms. Form fields will either contain data or be empty strings. There may be other fields, in the record, which are not on the form.
Our update procedure has parameters for every field in the table, the parameters default to NULL - except the PK fields which must be provided :)
An empty string field will therefore be provided to the procedure, and fields which are not on the form will not, and will default to NULL. It is not permitted to pass a form field as NULL - all fields that were on the form must pass a value.
For existing records, being updated, we treat NULL as "no change", and a value as causing a change. However, we do not allow storage of empty-string, so passing a parameter of an empty string causes the field in that record to be changed to NULL. (Particularly relevant to Date types, for example, as only valid dates can be stored, not empty strings; and empty string for a number would probably be treated as Zero if we did not apply this rule)
This has worked well for us and allowed us to mechanically generate such database procedures and the relevant Forms associated with them.
Additionally we allow the Update Procedure to return a Validation Error code. In general validation of form fields is carried out by the application - firstly using Javascript at the Client to enforce dates as being valid, and so on; secondly the application can apply the same (for safety) and additional validations. And finally the database procedure can perform further validations - such as to check that an Order Item has a corresponding Order Header record; whilst this is also enforced by a Foreign Key constraint, we do not like having to catch (and interpret) the database error that results, so the Update Procedure checks, and returns an error number / message which is useful to the application (and corresponds to a message, maintained in an Errors Table, which can be displayed to the Operator so they know what the problem was). The Foreign key constraint is still in place as a long-stop catcher.
We also mechanical generate the Get/Read procedure. This returns all fields for a single row. Parameter to the procedure is all PK fields. This can be used by CRUD forms to get any existing data. We use this, rather than a specific procedure for each form, in the knowledge that it retrieves all columns and some of them may not be required on the specific form. It is only a single row retrieve, and the likelihood is that all fields will be represented on the CRUD maintenance form. However, based on 80:20 ri;rule we are more circumspect on forms for records that have Text columns that are not on the form - clearly, retrieving many K of data which is not on the form is not good. For the remainder we feel that the consistency of the programming, and having relatively few exceptions, reduces errors and the cost saving outweighs any extra data retrieved.
This approach also means that as the database schema changes the Procedures are in-step with the changes, and will bring to light problems that this now causes with existing forms. For example if a column is renamed the form will immediately fail - by trying to call the procedure with a parameter that is no longer valid, or by using a column that is no longer retrieved. We can insulate for this where necessary for backwards compatibility - allow the Update procedure to have an additional parameter with the original name and COALESCE that with the parameter representing the column's new name, and have the Get/Read return duplicate columns in the resultset with both New and Old names.
Similarly for Delete. The procedure is mechanically generated, takes the PK as a parameter, and has the ability to return a Validation message (e.g. when trying to delete the Order Header if it still has child Order Item records).
All our tables have fields for Creator, Create date, Updater, Update date, and also a EditNo - which is incremented whenever the record is saved. The EditNo value is is given to the Form (in a Hidden Input field) and thus passed to either Update or Delete procedure. It must match the exist record's value, otherwise the record has been changed by another operator and the later update is rejected (again, Update procedures provide a useful message to the operator - including who the other updater was, and at what time).
For most data tables we also have an Archive table. This stores a copy of the "old" record when an update is made. We do not store the New record - because that is in the main table, and thus reduce the amount of data we store in the Archive.
This has all columns in the main record, plus an Action - Update or Delete - and the Audit Date. Records are inserted into the Archive by a Update/Delete Trigger on the main table.
We also have mechanically generated Find procedures. These have parameters which match fields in the table, but can be for Start/End points (e.g. an order date range), an Exact match, or a "contains" match - such as "Name like XXXX". These return only the specific fields which are actually used by the display, and the mechanically generated procedure has a suitable WHERE clause using the defined parameters. In practice these procedures get changed manually to be hand optimised etc. but are useful when first making an application to provide a flying-start for user query of data.