mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'postgres3' into develop
This commit is contained in:
commit
cc16156c84
|
@ -77,13 +77,7 @@ class Variable<T extends Object> extends Expression<T> {
|
|||
var suffix = '';
|
||||
if (context.dialect == SqlDialect.postgres) {
|
||||
explicitStart = 1;
|
||||
mark = '@';
|
||||
|
||||
if (value is List<int>) {
|
||||
// We need to explicitly bind the variable as byte array. Otherwise
|
||||
// a Uint8List like [1,2,3] would bind to "{1,2,3}" as bytes
|
||||
suffix += ":bytea";
|
||||
}
|
||||
mark = r'$';
|
||||
}
|
||||
|
||||
if (explicitStart != null) {
|
||||
|
|
|
@ -82,6 +82,16 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
return _out.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
bool isKeyword(String lexeme) {
|
||||
switch (options.effectiveDialect) {
|
||||
case SqlDialect.postgres:
|
||||
return isKeywordLexeme(lexeme) || isPostgresKeywordLexeme(lexeme);
|
||||
default:
|
||||
return isKeywordLexeme(lexeme);
|
||||
}
|
||||
}
|
||||
|
||||
FoundVariable? _findMoorVar(Variable target) {
|
||||
return query!.variables.firstWhereOrNull(
|
||||
(f) => f.variable.resolvedIndex == target.resolvedIndex);
|
||||
|
@ -91,7 +101,7 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
if (variable.isArray) {
|
||||
_writeRawInSpaces('(\$${expandedName(variable)})');
|
||||
} else {
|
||||
final mark = _isPostgres ? '@' : '?';
|
||||
final mark = _isPostgres ? '\\\$' : '?';
|
||||
_writeRawInSpaces('$mark${variable.index}');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift_dev
|
||||
description: Dev-dependency for users of drift. Contains the generator and development tools.
|
||||
version: 2.5.2
|
||||
version: 2.6.0-dev
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
@ -27,7 +27,7 @@ dependencies:
|
|||
# Drift-specific analysis and apis
|
||||
drift: '>=2.5.0 <2.6.0'
|
||||
sqlite3: '>=0.1.6 <2.0.0'
|
||||
sqlparser: '^0.27.0'
|
||||
sqlparser: '^0.28.0'
|
||||
|
||||
# Dart analysis
|
||||
analyzer: ^5.2.0
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/src/analysis/options.dart';
|
||||
import 'package:drift_dev/src/analysis/results/results.dart';
|
||||
import 'package:drift_dev/src/writer/queries/sql_writer.dart';
|
||||
|
@ -5,14 +6,14 @@ import 'package:sqlparser/sqlparser.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
void check(String sql, String expectedDart) {
|
||||
void check(String sql, String expectedDart,
|
||||
{DriftOptions options = const DriftOptions.defaults()}) {
|
||||
final engine = SqlEngine();
|
||||
final context = engine.analyze(sql);
|
||||
final query = SqlSelectQuery('name', context, context.root, [], [],
|
||||
InferredResultSet(null, []), null, null);
|
||||
|
||||
final result =
|
||||
SqlWriter(const DriftOptions.defaults(), query: query).write();
|
||||
final result = SqlWriter(options, query: query).write();
|
||||
|
||||
expect(result, expectedDart);
|
||||
}
|
||||
|
@ -28,4 +29,11 @@ void main() {
|
|||
test('escapes Dart characters in SQL', () {
|
||||
check(r"SELECT '$hey';", r"'SELECT \'\$hey\''");
|
||||
});
|
||||
|
||||
test('escapes postgres keywords', () {
|
||||
check('SELECT * FROM user', "'SELECT * FROM user'");
|
||||
check('SELECT * FROM user', "'SELECT * FROM \"user\"'",
|
||||
options: DriftOptions.defaults(
|
||||
dialect: DialectOptions(SqlDialect.postgres, null)));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:drift/backends.dart';
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart';
|
||||
import 'package:drift_postgres/postgres.dart';
|
||||
import 'package:postgres/postgres_v3_experimental.dart';
|
||||
|
||||
void main() async {
|
||||
final postgres = PgDatabase(
|
||||
endpoint: PgEndpoint(
|
||||
host: 'localhost',
|
||||
database: 'postgres',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
),
|
||||
logStatements: true,
|
||||
);
|
||||
|
||||
await postgres.ensureOpen(_NullUser());
|
||||
|
||||
final rows = await postgres.runSelect(r'SELECT $1', [true]);
|
||||
final row = rows.single;
|
||||
print(row);
|
||||
print(row.values.map((e) => e.runtimeType).toList());
|
||||
}
|
||||
|
||||
class _NullUser extends QueryExecutorUser {
|
||||
@override
|
||||
Future<void> beforeOpen(
|
||||
QueryExecutor executor, OpeningDetails details) async {}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
|
@ -1,13 +1,28 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/backends.dart';
|
||||
import 'package:postgres/postgres.dart';
|
||||
import 'package:postgres/postgres_v3_experimental.dart';
|
||||
|
||||
/// A drift database implementation that talks to a postgres database.
|
||||
class PgDatabase extends DelegatedDatabase {
|
||||
PgDatabase({
|
||||
required PgEndpoint endpoint,
|
||||
PgSessionSettings? sessionSettings,
|
||||
bool logStatements = false,
|
||||
}) : super(
|
||||
_PgDelegate(
|
||||
() => PgConnection.open(endpoint, sessionSettings: sessionSettings),
|
||||
true,
|
||||
),
|
||||
isSequential: true,
|
||||
logStatements: logStatements,
|
||||
);
|
||||
|
||||
/// Creates a drift database implementation from a postgres database
|
||||
/// [connection].
|
||||
PgDatabase(PostgreSQLConnection connection, {bool logStatements = false})
|
||||
: super(_PgDelegate(connection, connection),
|
||||
PgDatabase.opened(PgSession connection, {bool logStatements = false})
|
||||
: super(_PgDelegate(() => connection, false),
|
||||
isSequential: true, logStatements: logStatements);
|
||||
|
||||
@override
|
||||
|
@ -15,14 +30,12 @@ class PgDatabase extends DelegatedDatabase {
|
|||
}
|
||||
|
||||
class _PgDelegate extends DatabaseDelegate {
|
||||
final PostgreSQLConnection _db;
|
||||
final PostgreSQLExecutionContext _ec;
|
||||
|
||||
_PgDelegate(this._db, this._ec) : closeUnderlyingWhenClosed = false;
|
||||
|
||||
bool _isOpen = false;
|
||||
_PgDelegate(this._open, this.closeUnderlyingWhenClosed);
|
||||
|
||||
final bool closeUnderlyingWhenClosed;
|
||||
final FutureOr<PgSession> Function() _open;
|
||||
|
||||
PgSession? _openedSession;
|
||||
|
||||
@override
|
||||
TransactionDelegate get transactionDelegate => const NoTransactionDelegate();
|
||||
|
@ -31,47 +44,57 @@ class _PgDelegate extends DatabaseDelegate {
|
|||
late DbVersionDelegate versionDelegate;
|
||||
|
||||
@override
|
||||
Future<bool> get isOpen => Future.value(_isOpen);
|
||||
Future<bool> get isOpen => Future.value(_openedSession != null);
|
||||
|
||||
@override
|
||||
Future<void> open(QueryExecutorUser user) async {
|
||||
final pgVersionDelegate = _PgVersionDelegate(_db);
|
||||
final session = await _open();
|
||||
final pgVersionDelegate = _PgVersionDelegate(session);
|
||||
|
||||
await _db.open();
|
||||
await pgVersionDelegate.init();
|
||||
|
||||
_openedSession = session;
|
||||
versionDelegate = pgVersionDelegate;
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
Future _ensureOpen() async {
|
||||
if (_db.isClosed) {
|
||||
await _db.open();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(BatchedStatements statements) async {
|
||||
await _ensureOpen();
|
||||
final session = _openedSession!;
|
||||
final prepared =
|
||||
List<PgStatement?>.filled(statements.statements.length, null);
|
||||
|
||||
for (final row in statements.arguments) {
|
||||
final stmt = statements.statements[row.statementIndex];
|
||||
final args = row.arguments;
|
||||
try {
|
||||
for (final instantation in statements.arguments) {
|
||||
final pgArgs = _BoundArguments.ofDartArgs(instantation.arguments);
|
||||
|
||||
await _ec.execute(stmt, substitutionValues: _convertArgs(args));
|
||||
// Lazily prepare statements when we run into them. The reason is that
|
||||
// we need to know the types for variables.
|
||||
final stmtIndex = instantation.statementIndex;
|
||||
var stmt = prepared[stmtIndex];
|
||||
if (stmt == null) {
|
||||
final sql = statements.statements[stmtIndex];
|
||||
stmt = prepared[stmtIndex] =
|
||||
await session.prepare(PgSql(sql, types: pgArgs.types));
|
||||
}
|
||||
|
||||
return Future.value();
|
||||
await stmt.run(pgArgs.parameters);
|
||||
}
|
||||
} finally {
|
||||
for (final stmt in prepared) {
|
||||
await stmt?.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _runWithArgs(String statement, List<Object?> args) async {
|
||||
await _ensureOpen();
|
||||
final session = _openedSession!;
|
||||
|
||||
if (args.isEmpty) {
|
||||
return _ec.execute(statement);
|
||||
} else {
|
||||
return _ec.execute(statement, substitutionValues: _convertArgs(args));
|
||||
}
|
||||
final pgArgs = _BoundArguments.ofDartArgs(args);
|
||||
final result = await session.execute(
|
||||
PgSql(statement, types: pgArgs.types),
|
||||
parameters: pgArgs.parameters,
|
||||
);
|
||||
return result.affectedRows;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -81,16 +104,10 @@ class _PgDelegate extends DatabaseDelegate {
|
|||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List<Object?> args) async {
|
||||
await _ensureOpen();
|
||||
PostgreSQLResult result;
|
||||
if (args.isEmpty) {
|
||||
result = await _ec.query(statement);
|
||||
} else {
|
||||
result = await _ec.query(
|
||||
statement,
|
||||
substitutionValues: _convertArgs(args),
|
||||
);
|
||||
}
|
||||
final session = _openedSession!;
|
||||
final pgArgs = _BoundArguments.ofDartArgs(args);
|
||||
final result = await session.execute(PgSql(statement, types: pgArgs.types),
|
||||
parameters: pgArgs.parameters);
|
||||
return result.firstOrNull?[0] as int? ?? 0;
|
||||
}
|
||||
|
||||
|
@ -101,63 +118,96 @@ class _PgDelegate extends DatabaseDelegate {
|
|||
|
||||
@override
|
||||
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
||||
await _ensureOpen();
|
||||
final result = await _ec.query(
|
||||
statement,
|
||||
substitutionValues: _convertArgs(args),
|
||||
);
|
||||
final session = _openedSession!;
|
||||
final pgArgs = _BoundArguments.ofDartArgs(args);
|
||||
final result = await session.execute(PgSql(statement, types: pgArgs.types),
|
||||
parameters: pgArgs.parameters);
|
||||
|
||||
return Future.value(QueryResult.fromRows(
|
||||
result.map((e) => e.toColumnMap()).toList(growable: false)));
|
||||
return QueryResult([
|
||||
for (final pgColumn in result.schema.columns) pgColumn.columnName ?? '',
|
||||
], result);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (closeUnderlyingWhenClosed) {
|
||||
await _db.close();
|
||||
await _openedSession?.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object? _convertValue(Object? value) {
|
||||
if (value is BigInt) {
|
||||
return value.toInt();
|
||||
}
|
||||
return value;
|
||||
class _BoundArguments {
|
||||
final List<PgDataType> types;
|
||||
final List<PgTypedParameter> parameters;
|
||||
|
||||
_BoundArguments(this.types, this.parameters);
|
||||
|
||||
factory _BoundArguments.ofDartArgs(List<Object?> args) {
|
||||
final types = <PgDataType>[];
|
||||
final parameters = <PgTypedParameter>[];
|
||||
|
||||
void add(PgTypedParameter param) {
|
||||
types.add(param.type);
|
||||
parameters.add(param);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _convertArgs(List<Object?> args) {
|
||||
return args.asMap().map(
|
||||
(key, value) => MapEntry(
|
||||
(key + 1).toString(),
|
||||
_convertValue(value),
|
||||
),
|
||||
);
|
||||
for (final value in args) {
|
||||
if (value == null) {
|
||||
add(PgTypedParameter(PgDataType.text, null));
|
||||
} else if (value is int) {
|
||||
add(PgTypedParameter(PgDataType.bigInteger, value));
|
||||
} else if (value is BigInt) {
|
||||
// Drift only uses BigInts to represent 64-bit values on the web, so we
|
||||
// can use toInt() here.
|
||||
add(PgTypedParameter(PgDataType.bigInteger, value));
|
||||
} else if (value is bool) {
|
||||
add(PgTypedParameter(PgDataType.boolean, value));
|
||||
} else if (value is double) {
|
||||
add(PgTypedParameter(PgDataType.double, value));
|
||||
} else if (value is String) {
|
||||
add(PgTypedParameter(PgDataType.text, value));
|
||||
} else if (value is List<int>) {
|
||||
add(PgTypedParameter(PgDataType.byteArray, value));
|
||||
} else {
|
||||
throw ArgumentError.value(value, 'value', 'Unsupported type');
|
||||
}
|
||||
}
|
||||
|
||||
return _BoundArguments(types, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
class _PgVersionDelegate extends DynamicVersionDelegate {
|
||||
final PostgreSQLConnection database;
|
||||
final PgSession database;
|
||||
|
||||
_PgVersionDelegate(this.database);
|
||||
|
||||
@override
|
||||
Future<int> get schemaVersion async {
|
||||
final result = await database.query('SELECT version FROM __schema');
|
||||
final result =
|
||||
await database.execute(PgSql('SELECT version FROM __schema'));
|
||||
return result[0][0] as int;
|
||||
}
|
||||
|
||||
Future init() async {
|
||||
await database.query('CREATE TABLE IF NOT EXISTS __schema ('
|
||||
'version integer NOT NULL DEFAULT 0)');
|
||||
final count = await database.query('SELECT COUNT(*) FROM __schema');
|
||||
await database.execute(PgSql('CREATE TABLE IF NOT EXISTS __schema ('
|
||||
'version integer NOT NULL DEFAULT 0)'));
|
||||
|
||||
final count =
|
||||
await database.execute(PgSql('SELECT COUNT(*) FROM __schema'));
|
||||
if (count[0][0] as int == 0) {
|
||||
await database.query('INSERT INTO __schema (version) VALUES (0)');
|
||||
await database
|
||||
.execute(PgSql('INSERT INTO __schema (version) VALUES (0)'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setSchemaVersion(int version) async {
|
||||
await database.query('UPDATE __schema SET version = @1',
|
||||
substitutionValues: {'1': version});
|
||||
await database.execute(
|
||||
PgSql(r'UPDATE __schema SET version = $1', types: [PgDataType.integer]),
|
||||
parameters: [
|
||||
PgTypedParameter(PgDataType.integer, version),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ environment:
|
|||
dependencies:
|
||||
collection: ^1.16.0
|
||||
drift: ^2.0.0
|
||||
postgres: ^2.4.3
|
||||
postgres:
|
||||
meta: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -20,3 +20,6 @@ dev_dependencies:
|
|||
dependency_overrides:
|
||||
drift:
|
||||
path: ../../drift
|
||||
postgres:
|
||||
git:
|
||||
url: https://github.com/isoos/postgresql-dart.git
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift_postgres/postgres.dart';
|
||||
import 'package:drift_testcases/tests.dart';
|
||||
import 'package:postgres/postgres.dart';
|
||||
import 'package:postgres/postgres_v3_experimental.dart';
|
||||
|
||||
class PgExecutor extends TestExecutor {
|
||||
@override
|
||||
|
@ -11,9 +11,14 @@ class PgExecutor extends TestExecutor {
|
|||
|
||||
@override
|
||||
DatabaseConnection createConnection() {
|
||||
final pgConnection = PostgreSQLConnection('localhost', 5432, 'postgres',
|
||||
username: 'postgres', password: 'postgres');
|
||||
return DatabaseConnection(PgDatabase(pgConnection));
|
||||
return DatabaseConnection(PgDatabase(
|
||||
endpoint: PgEndpoint(
|
||||
host: 'localhost',
|
||||
database: 'postgres',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -554,7 +554,7 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
late final $FriendshipsTable friendships = $FriendshipsTable(this);
|
||||
Selectable<User> mostPopularUsers(int amount) {
|
||||
return customSelect(
|
||||
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT @1',
|
||||
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT \$1',
|
||||
variables: [
|
||||
Variable<int>(amount)
|
||||
],
|
||||
|
@ -566,7 +566,7 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
|
||||
Selectable<int> amountOfGoodFriends(int user) {
|
||||
return customSelect(
|
||||
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = @1 OR f.second_user = @1)',
|
||||
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = \$1 OR f.second_user = \$1)',
|
||||
variables: [
|
||||
Variable<int>(user)
|
||||
],
|
||||
|
@ -577,7 +577,7 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
|
||||
Selectable<FriendshipsOfResult> friendshipsOf(int user) {
|
||||
return customSelect(
|
||||
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS user ON user.id IN (f.first_user, f.second_user) AND user.id != @1 WHERE(f.first_user = @1 OR f.second_user = @1)',
|
||||
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS "user" ON "user".id IN (f.first_user, f.second_user) AND "user".id != \$1 WHERE(f.first_user = \$1 OR f.second_user = \$1)',
|
||||
variables: [
|
||||
Variable<int>(user)
|
||||
],
|
||||
|
@ -601,7 +601,7 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
}
|
||||
|
||||
Selectable<Preferences?> settingsFor(int user) {
|
||||
return customSelect('SELECT preferences FROM users WHERE id = @1',
|
||||
return customSelect('SELECT preferences FROM users WHERE id = \$1',
|
||||
variables: [
|
||||
Variable<int>(user)
|
||||
],
|
||||
|
@ -626,7 +626,7 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
|
||||
Future<List<Friendship>> returning(int var1, int var2, bool var3) {
|
||||
return customWriteReturning(
|
||||
'INSERT INTO friendships VALUES (@1, @2, @3) RETURNING *',
|
||||
'INSERT INTO friendships VALUES (\$1, \$2, \$3) RETURNING *',
|
||||
variables: [
|
||||
Variable<int>(var1),
|
||||
Variable<int>(var2),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
## 0.27.1-dev
|
||||
## 0.28.0-dev
|
||||
|
||||
- Support the `unhex` function added in sqlite 3.41.0
|
||||
- Support custom keyword sets when formatting SQL.
|
||||
|
||||
## 0.27.0
|
||||
|
||||
|
|
|
@ -24,6 +24,10 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
}
|
||||
}
|
||||
|
||||
bool isKeyword(String lexeme) {
|
||||
return isKeywordLexeme(lexeme);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitAggregateFunctionInvocation(
|
||||
AggregateFunctionInvocation e, void arg) {
|
||||
|
@ -1321,7 +1325,7 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
/// Writes an identifier, escaping it if necessary.
|
||||
void identifier(String identifier,
|
||||
{bool spaceBefore = true, bool spaceAfter = true}) {
|
||||
if (isKeywordLexeme(identifier) || _notAKeywordRegex.hasMatch(identifier)) {
|
||||
if (isKeyword(identifier) || _notAKeywordRegex.hasMatch(identifier)) {
|
||||
identifier = '"$identifier"';
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: sqlparser
|
||||
description: Parses sqlite statements and performs static analysis on them
|
||||
version: 0.27.1-dev
|
||||
version: 0.28.0-dev
|
||||
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
|
||||
repository: https://github.com/simolus3/drift
|
||||
#homepage: https://drift.simonbinder.eu/
|
||||
|
|
Loading…
Reference in New Issue