Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions typed_sql/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.1.14
* Support `upsertValue` as a shortcut to `insertValue` + `onConflict` + `update`.

## 0.1.13
* Support `CREATE INDEX` DDLs through `@Index` annotations.

Expand Down
69 changes: 69 additions & 0 deletions typed_sql/example/lib/src/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

143 changes: 92 additions & 51 deletions typed_sql/lib/src/codegen/build_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,62 @@ Iterable<Spec> buildTable(ParsedTable table, ParsedSchema schema) sync* {
]),
);

/// Parameters for `.insertValue()` and `.upsertValue()`: one named parameter per field.
///
/// Depending on whether a field has a default value and/or is nullable we
/// have the following cases:
///
/// 1. `!hasDefault && !isNullable`:
/// The user must give us a value!
/// The parameter is _required_ and non-nullable.
///
/// 2. `hasDefault && !isNullable`:
/// The user may give us a value, or we can omit the field
/// and the database will insert the _default value_.
/// The parameter is optional and nullable, null means omit
/// the field when inserting the row.
///
/// 3. `!hasDefault && isNullable`:
/// The user may give us a value, or we can insert `NULL` as
/// the default value -- this also what .insert() does!
/// The parameter is optional and nullable, null means set
/// the field `NULL` when inserting the row.
///
/// 4. `hasDefault && isNullable`:
/// The user may give us a value, or omit the field to get
/// _default value_, or the user may give `null` meaning
/// `NULL`. We cannot represent all 3 options!
/// We always intepret `null` as `NULL`, thus, the user
/// cannot omit the field and get the _default value_.
/// The user can use `.insert()` instead of `.insertValue()`.
/// This could be surprising which is why we have:
/// * A warning in the documentation, and,
/// * Decided that the parameter is _required_ and nullable.
/// The parameter is _required_ and _nullable_, so that
/// inserting `null` is explicit, not implicit!
///
/// NOTE: We do not set the default value as _default value_ in
/// dart, because this does not work for auto-increment or
/// for NOW() and similar annotations.
Iterable<Parameter> insertValueParameters() => rowClass.fields.map((field) {
final isOptional = field.hasDefault ^ field.isNullable;
final nullablePostfix = field.hasDefault || field.isNullable ? '?' : '';

return Parameter(
(b) => b
..name = field.name
..named = true
..required = !isOptional
..type = refer('${field.typeName}$nullablePostfix'),
);
});

// Fields that are not part of the _primary key_, these are the fields
// `.upsertValue()` will overwrite when a _primary key_ conflict occurs.
final nonPrimaryKeyFields = rowClass.fields
.where((field) => !rowClass.primaryKey.contains(field))
.toList();

// Extension for Table<Row>
yield Extension(
(b) => b
Expand Down Expand Up @@ -543,57 +599,7 @@ Iterable<Spec> buildTable(ParsedTable table, ParsedSchema schema) sync* {
Returns a [InsertSingle] statement on which `.execute` must be
called for the row to be inserted.
''')
..optionalParameters.addAll(
rowClass.fields.map((field) {
// Depending on whether a field has a default value and/or is
// nullable we have the following cases:
//
// 1. `!hasDefault && !isNullable`:
// The user must give us a value!
// The parameter is _required_ and non-nullable.
//
// 2. `hasDefault && !isNullable`:
// The user may give us a value, or we can omit the field
// and the database will insert the _default value_.
// The parameter is optional and nullable, null means omit
// the field when inserting the row.
//
// 3. `!hasDefault && isNullable`:
// The user may give us a value, or we can insert `NULL` as
// the default value -- this also what .insert() does!
// The parameter is optional and nullable, null means set
// the field `NULL` when inserting the row.
//
// 4. `hasDefault && isNullable`:
// The user may give us a value, or omit the field to get
// _default value_, or the user may give `null` meaning
// `NULL`. We cannot represent all 3 options!
// We always intepret `null` as `NULL`, thus, the user
// cannot omit the field and get the _default value_.
// The user can use `.insert()` instead of `.insertValue()`.
// This could be surprising which is why we have:
// * A warning in the documentation, and,
// * Decided that the parameter is _required_ and nullable.
// The parameter is _required_ and _nullable_, so that
// inserting `null` is explicit, not implicit!
//
// NOTE: We do not set the default value as _default value_ in
// dart, because this does not work for auto-increment or
// for NOW() and similar annotations.
final isOptional = field.hasDefault ^ field.isNullable;
final nullablePostfix = field.hasDefault || field.isNullable
? '?'
: '';

return Parameter(
(b) => b
..name = field.name
..named = true
..required = !isOptional
..type = refer('${field.typeName}$nullablePostfix'),
);
}),
)
..optionalParameters.addAll(insertValueParameters())
..returns = refer('InsertSingle<$rowClassName>')
..lambda = true
..body = Code('''
Expand All @@ -610,6 +616,41 @@ Iterable<Spec> buildTable(ParsedTable table, ParsedSchema schema) sync* {
'''),
),
)
..methods.add(
Method(
(b) => b
..name = 'upsertValue'
..documentation('''
Insert row into the `${table.name}` table, or update the
existing row if it conflicts with the _primary key_.

This is a shorthand for calling `.insertValue(...)` followed by
`.onConflict(.primaryKey)` and `.update(...)` to overwrite
${nonPrimaryKeyFields.isEmpty ? 'nothing, as all fields are part of the _primary key_,' : 'the fields ${nonPrimaryKeyFields.map((f) => '`${f.name}`').join(', ')},'}
with the values given, leaving the _primary key_ untouched.

${rowClass.fields.whereDefaultAndNullable.isEmpty ? '' : '''\n
> [!WARNING]
> It is not possible to insert the _default value_ for fields that
> are nullable. Providing `null` will insert `NULL` for
> ${rowClass.fields.whereDefaultAndNullable.map((f) => '`${f.name}`').join(', ')}.
'''}

Returns an [UpsertSingle] statement on which `.execute()` must be
called for the row to be inserted or updated.
''')
..optionalParameters.addAll(insertValueParameters())
..returns = refer('UpsertSingle<$rowClassName>')
..lambda = true
..body = Code('''
insertValue(
${rowClass.fields.map((field) => '${field.name}: ${field.name}').join(', ')},
).onConflict(.primaryKey).update(
(_, excluded, set) => set(${nonPrimaryKeyFields.map((f) => '${f.name}: excluded.${f.name}').join(', ')}),
)
'''),
),
)
..methods.add(
Method(
(b) => b
Expand Down
7 changes: 7 additions & 0 deletions typed_sql/lib/src/dialect/postgres_dialect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ final class _PostgresDialect extends SqlDialect {
'(${c.conflictTarget.map(escape).join(', ')})',
'DO NOTHING',
].join(' ');
case final UpdateOnConflictClause c when c.columns.isEmpty:
// `DO UPDATE SET` requires at least one column, using `DO NOTHING` as no-op.
conflictClause = [
'ON CONFLICT',
'(${c.conflictTarget.map(escape).join(', ')})',
'DO NOTHING',
].join(' ');
case final UpdateOnConflictClause c:
final r = resolver
.withScope(
Expand Down
7 changes: 7 additions & 0 deletions typed_sql/lib/src/dialect/sqlite_dialect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ final class _Sqlite extends SqlDialect {
'(${c.conflictTarget.map(escape).join(', ')})',
'DO NOTHING',
].join(' ');
case final UpdateOnConflictClause c when c.columns.isEmpty:
// `DO UPDATE SET` requires at least one column, using `DO NOTHING` as no-op.
conflictClause = [
'ON CONFLICT',
'(${c.conflictTarget.map(escape).join(', ')})',
'DO NOTHING',
].join(' ');
case final UpdateOnConflictClause c:
final r = resolver
.withScope(
Expand Down
2 changes: 1 addition & 1 deletion typed_sql/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: typed_sql
version: 0.1.13
version: 0.1.14
description: Package for doing SQL with some type safety.
homepage: https://github.com/google/dart-neats/tree/master/typed_sql
repository: https://github.com/google/dart-neats.git
Expand Down
69 changes: 69 additions & 0 deletions typed_sql/test/sqlite/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading