Skip to content

Commit

Permalink
feat(postgrest): updates for postgREST 11 (#550)
Browse files Browse the repository at this point in the history
* feat: add all and any filter

* feat: add defaultToNull

* test: add new filter tests

* fix: bulk insert with missing column

* ci: use postgres 15

* test: add defaultToNull test

* test: fix basic update

* fix: comment for ilike

* docs: prevent loose list in readme

* docs: update insert/upsert doc

* test: remove failing rls test

* test: add more insert tests

* fix: typo

* docs: update insert upsert comment
  • Loading branch information
Vinzent03 committed Jul 23, 2023
1 parent 5b04b76 commit 64d8eb5
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -11,7 +11,7 @@ Monorepo containing all [Supabase](https://supabase.com/) libraries for Flutter.
- [supabase_flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter)
- [yet_another_json_isolate](https://github.com/supabase/supabase-flutter/tree/main/packages/yet_another_json_isolate)

- Documentation: https://supabase.com/docs/reference/dart/introduction
Documentation: https://supabase.com/docs/reference/dart/introduction

---

Expand Down
4 changes: 2 additions & 2 deletions infra/postgrest/docker-compose.yml
Expand Up @@ -3,7 +3,7 @@
version: '3'
services:
rest:
image: postgrest/postgrest:v9.0.1.20220802
image: postgrest/postgrest:v11.0.0
ports:
- '3000:3000'
environment:
Expand All @@ -16,7 +16,7 @@ services:
depends_on:
- db
db:
image: supabase/postgres:14.1.0.34
image: supabase/postgres:15.1.0.37
ports:
- '5432:5432'
volumes:
Expand Down
52 changes: 52 additions & 0 deletions packages/postgrest/lib/src/postgrest_filter_builder.dart
Expand Up @@ -152,6 +152,32 @@ class PostgrestFilterBuilder<T> extends PostgrestTransformBuilder<T> {
return this;
}

/// Match only rows where [column] matches all of [patterns] case-sensitively.
///
/// ```dart
/// await supabase
/// .from('users')
/// .select()
/// .likeAllOf('username', ['%supa%', '%bot%']);
/// ```
PostgrestFilterBuilder likeAllOf(String column, List<String> patterns) {
appendSearchParams(column, 'like(all).{${patterns.join(',')}}');
return this;
}

/// Match only rows where [column] matches any of [patterns] case-sensitively.
///
/// ```dart
/// await supabase
/// .from('users')
/// .select()
/// .likeAnyOf('username', ['%supa%', '%bot%']);
/// ```
PostgrestFilterBuilder likeAnyOf(String column, List<String> patterns) {
appendSearchParams(column, 'like(any).{${patterns.join(',')}}');
return this;
}

/// Finds all rows whose value in the stated [column] matches the supplied [pattern] (case insensitive).
///
/// ```dart
Expand All @@ -165,6 +191,32 @@ class PostgrestFilterBuilder<T> extends PostgrestTransformBuilder<T> {
return this;
}

/// Match only rows where [column] matches all of [patterns] case-insensitively.
///
/// ```dart
/// await supabase
/// .from('users')
/// .select()
/// .ilikeAllOf('username', ['%supa%', '%bot%']);
/// ```
PostgrestFilterBuilder ilikeAllOf(String column, List<String> patterns) {
appendSearchParams(column, 'ilike(all).{${patterns.join(',')}}');
return this;
}

/// Match only rows where [column] matches any of [patterns] case-insensitively.
///
/// ```dart
/// await supabase
/// .from('users')
/// .select()
/// .ilikeAnyOf('username', ['%supa%', '%bot%']);
/// ```
PostgrestFilterBuilder ilikeAnyOf(String column, List<String> patterns) {
appendSearchParams(column, 'ilike(any).{${patterns.join(',')}}');
return this;
}

/// A check for exact equality (null, true, false)
///
/// Finds all rows whose value on the stated [column] exactly match the specified [value].
Expand Down
48 changes: 47 additions & 1 deletion packages/postgrest/lib/src/postgrest_query_builder.dart
Expand Up @@ -94,6 +94,12 @@ class PostgrestQueryBuilder<T> extends PostgrestBuilder<T, T> {
///
/// By default no data is returned. Use a trailing `select` to return data.
///
/// When inserting multiple rows in bulk, [defaultToNull] is used to set the values of fields missing in a proper subset of rows
/// to be either `NULL` or the default value of these columns.
/// Fields missing in all rows always use the default value of these columns.
///
/// For single row insertions, missing fields will be set to default values when applicable.
///
/// Default (not returning data):
/// ```dart
/// await supabase.from('messages').insert(
Expand All @@ -108,10 +114,23 @@ class PostgrestQueryBuilder<T> extends PostgrestBuilder<T, T> {
/// 'channel_id': 1
/// }).select();
/// ```
PostgrestFilterBuilder<T> insert(dynamic values) {
PostgrestFilterBuilder<T> insert(
dynamic values, {
bool defaultToNull = true,
}) {
_method = METHOD_POST;
_headers['Prefer'] = '';

if (!defaultToNull) {
_headers['Prefer'] = 'missing=default';
}

_body = values;

if (values is List) {
_setColumnsSearchParam(values);
}

return PostgrestFilterBuilder<T>(this);
}

Expand All @@ -122,6 +141,12 @@ class PostgrestQueryBuilder<T> extends PostgrestBuilder<T, T> {
///
/// By default no data is returned. Use a trailing `select` to return data.
///
/// When inserting multiple rows in bulk, [defaultToNull] is used to set the values of fields missing in a proper subset of rows
/// to be either `NULL` or the default value of these columns.
/// Fields missing in all rows always use the default value of these columns.
///
/// For single row insertions, missing fields will be set to default values when applicable.
///
/// Default (not returning data):
/// ```dart
/// await supabase.from('messages').upsert({
Expand All @@ -144,11 +169,17 @@ class PostgrestQueryBuilder<T> extends PostgrestBuilder<T, T> {
dynamic values, {
String? onConflict,
bool ignoreDuplicates = false,
bool defaultToNull = true,
FetchOptions options = const FetchOptions(),
}) {
_method = METHOD_POST;
_headers['Prefer'] =
'resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates';

if (!defaultToNull) {
_headers['Prefer'] = _headers['Prefer']! + ',missing=default';
}

if (onConflict != null) {
_url = _url.replace(
queryParameters: {
Expand All @@ -157,6 +188,11 @@ class PostgrestQueryBuilder<T> extends PostgrestBuilder<T, T> {
},
);
}

if (values is List) {
_setColumnsSearchParam(values);
}

_body = values;
_options = options.ensureNotHead();
return PostgrestFilterBuilder<T>(this);
Expand Down Expand Up @@ -223,4 +259,14 @@ class PostgrestQueryBuilder<T> extends PostgrestBuilder<T, T> {
_options = options.ensureNotHead();
return PostgrestFilterBuilder<T>(this);
}

void _setColumnsSearchParam(List values) {
final newValues = PostgrestList.from(values);
final columns = newValues.fold<List<String>>(
[], (value, element) => value..addAll(element.keys));
if (newValues.isNotEmpty) {
final uniqueColumns = {...columns}.map((e) => '"$e"').join(',');
appendSearchParams("columns", uniqueColumns);
}
}
}
104 changes: 68 additions & 36 deletions packages/postgrest/test/basic_test.dart
Expand Up @@ -125,6 +125,37 @@ void main() {
expect(res, isEmpty);
});

test('insert', () async {
final res = await postgrest.from('users').insert(
{
'username': "bot",
'status': 'OFFLINE',
},
).select<PostgrestList>();
expect(res.length, 1);
expect(res.first['status'], 'OFFLINE');
});

test('insert uses default value', () async {
final res = await postgrest.from('users').insert(
{
'username': "bot",
},
).select<PostgrestList>();
expect(res.length, 1);
expect(res.first['status'], 'ONLINE');
});

test('bulk insert with one row uses default value', () async {
final res = await postgrest.from('users').insert(
{
'username': "bot",
},
).select<PostgrestList>();
expect(res.length, 1);
expect(res.first['status'], 'ONLINE');
});

test('bulk insert', () async {
final res = await postgrest.from('messages').insert([
{'id': 4, 'message': 'foo', 'username': 'supabot', 'channel_id': 2},
Expand All @@ -133,6 +164,41 @@ void main() {
expect(res.length, 2);
});

test('bulk insert without column defaults', () async {
final res = await postgrest.from('users').insert(
[
{
'username': "bot",
'status': 'OFFLINE',
},
{
'username': "crazy bot",
},
],
).select<PostgrestList>();
expect(res.length, 2);
expect(res.first['status'], 'OFFLINE');
expect(res.last['status'], null);
});

test('bulk insert with column defaults', () async {
final res = await postgrest.from('users').insert(
[
{
'username': "bot",
'status': 'OFFLINE',
},
{
'username': "crazy bot",
},
],
defaultToNull: false,
).select<PostgrestList>();
expect(res.length, 2);
expect(res.first['status'], 'OFFLINE');
expect(res.last['status'], 'ONLINE');
});

test('basic update', () async {
final res = await postgrest
.from('messages')
Expand All @@ -141,33 +207,8 @@ void main() {
)
.is_("data", null)
.select<PostgrestList>();
expect(res, [
{
'id': 1,
'data': null,
'message': 'Hello World 👋',
'username': 'supabot',
'channel_id': 2,
'inserted_at': '2021-06-25T04:28:21.598+00:00'
},
{
'id': 2,
'data': null,
'message':
'Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.',
'username': 'supabot',
'channel_id': 2,
'inserted_at': '2021-06-29T04:28:21.598+00:00'
},
{
'id': 3,
'data': null,
'message': 'Supabase Launch Week is on fire',
'username': 'supabot',
'channel_id': 2,
'inserted_at': '2021-06-20T04:28:21.598+00:00'
}
]);
expect(res, isNotEmpty);
expect(res, everyElement(containsPair("channel_id", 2)));

final messages = await postgrest.from('messages').select<PostgrestList>();
for (final rec in messages) {
Expand Down Expand Up @@ -350,15 +391,6 @@ void main() {
expect(res.count, 1);
});

test('row level security error', () async {
try {
await postgrest.from('sample').update({'id': 2});
fail('Returned even with row level security');
} on PostgrestException catch (error) {
expect(error.code, '404');
}
});

test('withConverter', () async {
final res = await postgrest
.from('users')
Expand Down
51 changes: 51 additions & 0 deletions packages/postgrest/test/filter_test.dart
Expand Up @@ -183,6 +183,31 @@ void main() {
}
});

test('likeAllOf', () async {
PostgrestList res = await postgrest
.from('users')
.select<PostgrestList>('username')
.likeAllOf('username', ['%supa%', '%bot%']);
expect(res, isNotEmpty);
for (final item in res) {
expect(item['username'], contains('supa'));
expect(item['username'], contains('bot'));
}
});

test('likeAnyOf', () async {
PostgrestList res = await postgrest
.from('users')
.select<PostgrestList>('username')
.likeAnyOf('username', ['%supa%', '%wai%']);
expect(res, isNotEmpty);
for (final item in res) {
expect(
item['username'].contains('supa') || item['username'].contains('wai'),
true);
}
});

test('ilike', () async {
final res = await postgrest
.from('users')
Expand All @@ -195,6 +220,32 @@ void main() {
}
});

test('ilikeAllOf', () async {
PostgrestList res = await postgrest
.from('users')
.select<PostgrestList>('username')
.ilikeAllOf('username', ['%SUPA%', '%bot%']);
expect(res, isNotEmpty);
for (final item in res) {
expect(item['username'].toLowerCase(), contains('supa'));
expect(item['username'].toLowerCase(), contains('bot'));
}
});

test('ilikeAnyOf', () async {
PostgrestList res = await postgrest
.from('users')
.select<PostgrestList>('username')
.ilikeAnyOf('username', ['%SUPA%', '%wai%']);
expect(res, isNotEmpty);
for (final item in res) {
expect(
item['username'].toLowerCase().contains('supa') ||
item['username'].toLowerCase().contains('wai'),
true);
}
});

test('is', () async {
final res = await postgrest
.from('users')
Expand Down

0 comments on commit 64d8eb5

Please sign in to comment.