Skip to content

Proposal: Orthogonal returning(ids) + strong-typed conflict policy tags for sqlgen::insert #109

@Perdixky

Description

@Perdixky

Motivation

I’d like to add an API to collect auto-increment ids (rowid / integer primary key) after insertion, especially for bulk inserts.

At the same time, I want to keep insert-related behaviors orthogonal and composable. Instead of having dedicated functions like insert_or_replace, I propose strong-typed modifiers that can be composed (pipe style) or passed directly (direct style).

A key point: I prefer conflict policies to be strong types (tag types) rather than bool flags or enum class, so we can enforce valid combinations at compile time and keep room for future static checks (e.g., conflict target must be unique).


Goals

  • Add sqlgen::returning(ids) to collect inserted ids, composable with insert builder.
  • Redesign conflict handling with orthogonal modifiers (e.g. or_replace()), deprecating insert_or_replace (or making it a thin wrapper).
  • Avoid insert(..., true) style flags; use strong-typed tags to improve readability and prevent misuse.
  • Optional: support a stronger form that can be statically checked when schema/constraints are represented in types.

Proposed User-facing API

1) Returning inserted ids

Pipe style:

std::vector<std::uint64_t> ids;

sqlgen::sqlite::connect("database.db")
  .and_then(sqlgen::insert(people) | sqlgen::returning(ids))
  .value();

Direct style (equivalent):

std::vector<std::uint64_t> ids;

sqlgen::sqlite::connect("database.db")
  .and_then(sqlgen::insert(people, sqlgen::returning(ids)))
  .value();

2) Conflict policy as orthogonal modifier (strong typed)

Pipe style:

sqlgen::sqlite::connect("database.db")
  .and_then(sqlgen::insert(people) | sqlgen::or_replace())
  .value();

Direct style (equivalent):

sqlgen::sqlite::connect("database.db")
  .and_then(sqlgen::insert(people, sqlgen::or_replace()))
  .value();

3) Composition works naturally

std::vector<std::uint64_t> ids;

sqlgen::sqlite::connect("database.db")
  .and_then(sqlgen::insert(people) | sqlgen::or_replace() | sqlgen::returning(ids))
  .value();

Proposed Types / Signatures (sketch)

returning(ids)

namespace sqlgen {

template <class IdsContainer>
auto returning(IdsContainer& ids); // stores ref internally

} // namespace sqlgen

strong-typed conflict policy tags

namespace sqlgen::conflict_policy {
  struct error {};
  struct replace {};
  struct ignore {};
  template <typename T>
    concept ConflictPolicy = std::is_same_v<error, T>      ||
                                                std::is_same_v<replace, T> ||
                                                std::is_same_v<ignore, T> 
}

conflict modifier / sugar

namespace sqlgen {

inline auto or_replace() { return conflict_policy::replace{}; }
inline auto or_ignore()  { return conflict_policy::ignore{}; }

} // namespace sqlgen

insert overload accepting modifiers

namespace sqlgen {

template <class Rows, class... Modifiers>
auto insert(Rows&& rows, Modifiers&&... mods);

} // namespace sqlgen

Constraints/concepts can ensure only valid modifiers are accepted, and modifiers are only composable with insert statements.


Semantics notes

Returning ids (bulk insert)

Suggested semantics: 1:1 positional mapping

  • ids.size() == number_of_input_rows
  • ids[i] corresponds to the i-th row of the input sequence

Constraint Note: returning() is incompatible with or_ignore(). In SQLite, INSERT OR IGNORE silently skips rows on conflict, which would result in a mismatch between the number of input rows and the number of returned IDs. To maintain positional mapping integrity, this combination is disallowed at compile time.

SQLite OR REPLACE

SQLite INSERT OR REPLACE is effectively “delete conflicting row, then insert a new row”.
So ids returned by returning(ids) refer to the new inserted row, and may differ from old ids. This should be documented.


Compatibility / Migration

  • Deprecate insert_or_replace(...) in docs/examples.

  • Optionally keep it as a thin wrapper:

    • insert_or_replace(x)insert(x) | or_replace()

What do you think of this proposal? I’d appreciate any feedback or suggestions. I'm excited to contribute this to the project! 😁

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions