Use sqlparser for drift snippets

This commit is contained in:
Simon Binder 2022-02-14 22:13:00 +01:00
parent 99172c1216
commit 48041512cb
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 307 additions and 109 deletions

View File

@ -12,15 +12,18 @@ jobs:
- name: Get dependencies
run: dart pub get
working-directory: docs
- name: Analyze Dart sources
working-directory: docs
run: dart analyze --fatal-infos --fatal-warnings
- name: Run build
env:
IS_RELEASE: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/latest-release' }}
run: dart run tool/ci_build.dart
working-directory: docs
- name: Analyze Dart sources
working-directory: docs
run: >
dart analyze --fatal-infos --fatal-warnings
dart run drift_dev analyze
- name: Deploy to netlify (Branch)
if: ${{ github.event_name == 'push' }}
uses: nwtgck/actions-netlify@v1.2

View File

@ -4,16 +4,23 @@ builders:
build_to: cache
builder_factories: ["writeVersions"]
build_extensions: {"$package$": ["lib/versions.json"]}
code_snippets:
import: 'tool/snippets.dart'
build_to: cache
builder_factories: ["SnippetsBuilder.new"]
build_extensions: {"": [".excerpt.json"]}
auto_apply: none
targets:
prepare:
builders:
"|versions":
enabled: true
code_snippets:
"|code_snippets":
enabled: true
generate_for:
- "lib/snippets/**/*.dart"
- "lib/snippets/*/*.drift"
- "lib/snippets/*.dart"
auto_apply_builders: false
sources:
@ -21,6 +28,7 @@ targets:
- "lib/versions.json"
- "lib/snippets/**"
- "tool/write_versions.dart"
- "tool/snippets.dart"
$default:
dependencies: [":prepare"]
@ -38,8 +46,6 @@ targets:
- "--csp"
moor_generator:
enabled: false
code_snippets:
enabled: false
sources:
- "lib/**"
- "pages/**"

View File

@ -0,0 +1,27 @@
// #docregion overview
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'database.g.dart';
@DriftDatabase(
include: {'tables.drift'},
)
class MyDb extends _$MyDb {
// This example creates a simple in-memory database (without actual
// persistence).
// To store data, see the database setups from other "Getting started" guides.
MyDb() : super(NativeDatabase.memory());
@override
int get schemaVersion => 1;
}
// #enddocregion overview
extension MoreSnippets on MyDb {
// #docregion dart_interop_insert
Future<void> insert(TodosCompanion companion) async {
await into(todos).insert(companion);
}
// #enddocregion dart_interop_insert
}

View File

@ -0,0 +1,44 @@
/* #docregion overview */
CREATE TABLE coordinates (
id INTEGER NOT NULL PRIMARY KEY,
lat REAL NOT NULL,
long REAL NOT NULL
);
CREATE TABLE saved_routes (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
"from" INTEGER NOT NULL REFERENCES coordinates (id),
"to" INTEGER NOT NULL REFERENCES coordinates (id)
);
/* #enddocregion overview */
/* #docregion route_points*/
CREATE TABLE route_points (
route INTEGER NOT NULL REFERENCES saved_routes (id),
point INTEGER NOT NULL REFERENCES coordinates (id),
index_on_route INTEGER,
PRIMARY KEY (route, point)
);
/* #enddocregion route_points */
/* #docregion overview */
routesWithPoints: SELECT r.id, r.name, f.*, t.* FROM routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r."to";
/* #enddocregion overview */
/* #docregion nested */
routesWithNestedPoints: SELECT r.id, r.name, f.** AS "from", t.** AS "to" FROM routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r."to";
/* #enddocregion nested */
/* #docregion list */
routeWithPoints: SELECT
route.**,
LIST(SELECT coordinates.* FROM route_points
INNER JOIN coordinates ON id = point
WHERE route = route.id
ORDER BY index_on_route
) AS points
FROM saved_routes route;
/* #enddocregion list */

View File

@ -0,0 +1,19 @@
/* #docregion import */
import 'tables.drift'; -- single quotes are required for imports
/* #enddocregion import */
/* #docregion q1 */
myQuery(:variable AS TEXT): SELECT :variable;
/* #enddocregion q1 */
/* #docregion q2 */
myNullableQuery(:variable AS TEXT OR NULL): SELECT :variable;
/* #enddocregion q2 */
/* #docregion q3 */
myRequiredQuery(REQUIRED :variable AS TEXT OR NULL): SELECT :variable;
/* #enddocregion q3 */
/* #docregion entries */
entriesWithId: SELECT * FROM todos WHERE id IN ?;
/* #enddocregion entries */
/* #docregion filter */
_filterTodos: SELECT * FROM todos WHERE $predicate;
/* #enddocregion filter */

View File

@ -0,0 +1,19 @@
CREATE TABLE todos (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category INTEGER REFERENCES categories(id)
);
CREATE TABLE categories (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL
) AS Category; -- the AS xyz after the table defines the data class name
-- You can also create an index or triggers with drift files
CREATE INDEX categories_description ON categories(description);
-- we can put named sql queries in here as well:
createEntry: INSERT INTO todos (title, content) VALUES (:title, :content);
deleteById: DELETE FROM todos WHERE id = :id;
allTodos: SELECT * FROM todos;

View File

@ -1,4 +1,3 @@
// #docregion
import 'dart:ffi';
import 'dart:io';
import 'package:sqlite3/open.dart';
@ -15,4 +14,3 @@ DynamicLibrary _openOnLinux() {
return DynamicLibrary.open(libraryNextToScript.path);
}
// _openOnWindows could be implemented similarly by opening `sqlite3.dll`
// #enddocregion

View File

@ -11,52 +11,21 @@ aliases:
template: layouts/docs/single
---
{% assign dart_snippets = "package:moor_documentation/snippets/drift_files/database.dart.excerpt.json" | readString | json_decode %}
{% assign drift_tables = "package:moor_documentation/snippets/drift_files/tables.drift.excerpt.json" | readString | json_decode %}
{% assign small = "package:moor_documentation/snippets/drift_files/small_snippets.drift.excerpt.json" | readString | json_decode %}
Drift files are a new feature that lets you write all your database code in SQL - drift will generate typesafe APIs for them.
## Getting started
To use this feature, lets create two files: `database.dart` and `tables.drift`. The Dart file only contains the minimum code
to setup the database:
```dart
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'database.g.dart';
@DriftDatabase(
include: {'tables.drift'},
)
class MyDb extends _$MyDb {
// This example creates a simple in-memory database (without actual persistence).
// To actually store data, see the database setups from other "Getting started" guides.
MyDb() : super(NativeDatabase.memory());
@override
int get schemaVersion => 1;
}
```
{% include "blocks/snippet" snippets = dart_snippets name = "overview" %}
We can now declare tables and queries in the drift file:
```sql
CREATE TABLE todos (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category INTEGER REFERENCES categories(id)
);
CREATE TABLE categories (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL
) AS Category; -- the AS xyz after the table defines the data class name
-- You can also create an index or triggers with drift files
CREATE INDEX categories_description ON categories(description);
-- we can put named sql queries in here as well:
createEntry: INSERT INTO todos (title, content) VALUES (:title, :content);
deleteById: DELETE FROM todos WHERE id = :id;
allTodos: SELECT * FROM todos;
```
{% include "blocks/snippet" snippets = drift_tables %}
After running the build runner with `flutter pub run build_runner build`,
drift will write the `database.g.dart`
@ -92,22 +61,17 @@ queries, the variables will be written as parameters to your method.
When it's ambiguous, the analyzer might be unable to resolve the type of
a variable. For those scenarios, you can also denote the explicit type
of a variable:
```sql
myQuery(:variable AS TEXT): SELECT :variable;
```
{% include "blocks/snippet" snippets = small name = "q1" %}
In addition to the base type, you can also declare that the type is nullable:
```sql
myQuery(:variable AS TEXT OR NULL): SELECT :variable;
```
{% include "blocks/snippet" snippets = small name = "q2" %}
Finally, you can declare that a variable should be required in Dart when using
named parameters. To do so, add a `REQUIRED` keyword:
```sql
myQuery(REQUIRED :variable AS TEXT OR NULL): SELECT :variable;
```
{% include "blocks/snippet" snippets = small name = "q3" %}
Note that this only has an effect when the `named_parameters`
[build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}) is
@ -116,9 +80,9 @@ enabled. Further, non-nullable variables are required by default.
### Arrays
If you want to check whether a value is in an array of values, you can
use `IN ?`. That's not valid sql, but drift will desugar that at runtime. So, for this query:
```sql
entriesWithId: SELECT * FROM todos WHERE id IN ?;
```
{% include "blocks/snippet" snippets = small name = "entries" %}
Drift will generate a `Selectable<Todo> entriesWithId(List<int> ids)` method.
Running `entriesWithId([1,2])` would generate `SELECT * ... id IN (?1, ?2)` and
bind the arguments accordingly. To make sure this works as expected, drift
@ -142,9 +106,9 @@ written as an `INTEGER` column when the table gets created.
## Imports
You can put import statements at the top of a `drift` file:
```sql
import 'other.drift'; -- single quotes are required for imports
```
{% include "blocks/snippet" snippets = small name = "import" %}
All tables reachable from the other file will then also be visible in
the current file and to the database that `includes` it. If you want
to declare queries on tables that were defined in another drift
@ -161,28 +125,13 @@ know from Dart.
## Nested results
{% assign nested = "package:moor_documentation/snippets/drift_files/nested.drift.excerpt.json" | readString | json_decode %}
Many queries fetch all columns from some table, typically by using the
`SELECT table.*` syntax. That approach can become a bit tedious when applied
over multiple tables from a join, as shown in this example:
```sql
CREATE TABLE coordinates (
id INTEGER NOT NULL PRIMARY KEY,
lat REAL NOT NULL,
long REAL NOT NULL
);
CREATE TABLE saved_routes (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
"from" INTEGER NOT NULL REFERENCES coordinates (id),
"to" INTEGER NOT NULL REFERENCES coordinates (id)
);
routesWithPoints: SELECT r.id, r.name, f.*, t.* FROM routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r."to";
```
{% include "blocks/snippet" snippets = nested name = "overview" %}
To match the returned column names while avoiding name clashes in Dart, drift
will generate a class having an `id`, `name`, `id1`, `lat`, `long`, `lat1` and
@ -190,11 +139,7 @@ a `long1` field.
Of course, that's not helpful at all - was `lat1` coming from `from` or `to`
again? Let's rewrite the query, this time using nested results:
```sql
routesWithNestedPoints: SELECT r.id, r.name, f.**, t.** FROM routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r."to";
```
{% include "blocks/snippet" snippets = nested name = "nested" %}
As you can see, we can nest a result simply by using the drift-specific
`table.**` syntax.
@ -235,29 +180,13 @@ Re-using the `coordinates` and `saved_routes` tables introduced in the example
for [nested results](#nested-results), we add a new table storing coordinates
along a route:
```sql
CREATE TABLE route_points (
route INTEGER NOT NULL REFERENCES saved_routes (id),
point INTEGER NOT NULL REFERENCES coordinates (id),
index_on_route INTEGER,
PRIMARY KEY (route, point)
);
```
{% include "blocks/snippet" snippets = nested name = "route_points" %}
Now, assume we wanted to query a route with information about all points
along the way. While this requires two SQL statements, we can write this as a
single drift query that is then split into the two statements automatically:
```sql
routeWithPoints: SELECT
route.**
LIST(SELECT coordinates.* FROM route_points
INNER JOIN coordinates ON id = point
WHERE route = route.id
ORDER BY index_on_route
) AS points
FROM saved_routes route;
```
{% include "blocks/snippet" snippets = nested name = "list" %}
This will generate a result set containing a `SavedRoute route` field along with a
`List<Point> points` list of all points along the route.
@ -279,11 +208,9 @@ supported with the `new_sql_code_generation` [build option]({{ '../Advanced Feat
Drift files work perfectly together with drift's existing Dart API:
- you can write Dart queries for tables declared in a drift file:
```dart
Future<void> insert(TodosCompanion companion) async {
await into(todos).insert(companion);
}
```
{% include "blocks/snippet" snippets = dart_snippets name = "dart_interop_insert" %}
- by importing Dart files into a drift file, you can write sql queries for
tables declared in Dart.
- generated methods for queries can be used in transactions, they work

View File

@ -22,7 +22,7 @@ dev_dependencies:
shelf_static: ^1.1.0
code_snippets:
hosted: https://simonbinder.eu
version: ^0.0.2
version: ^0.0.3
# Fake path_provider for snippets
path_provider:

156
docs/tool/snippets.dart Normal file
View File

@ -0,0 +1,156 @@
import 'package:build/build.dart';
import 'package:code_snippets/builder.dart';
import 'package:code_snippets/highlight.dart';
import 'package:source_span/source_span.dart';
import 'package:sqlparser/sqlparser.dart';
class SnippetsBuilder extends CodeExcerptBuilder {
// ignore: avoid_unused_constructor_parameters
SnippetsBuilder([BuilderOptions? options]);
@override
bool shouldEmitFor(AssetId input, Excerpter excerpts) {
return true;
}
@override
Future<Highlighter?> highlighterFor(
AssetId assetId, String content, BuildStep buildStep) async {
switch (assetId.extension) {
case '.drift':
return _DriftHighlighter(
SourceFile.fromString(content, url: assetId.uri));
default:
return super.highlighterFor(assetId, content, buildStep);
}
}
}
class _DriftHighlighter extends Highlighter {
_DriftHighlighter(SourceFile file) : super(file);
@override
void highlight() {
final engine = SqlEngine(
EngineOptions(
useMoorExtensions: true,
version: SqliteVersion.current,
),
);
final result = engine.parseMoorFile(file.span(0).text);
_HighlightingVisitor().visit(result.rootNode, this);
for (final token in result.tokens) {
const ignoredKeyword = [
TokenType.$null,
TokenType.$true,
TokenType.$false
];
if (token is KeywordToken &&
!ignoredKeyword.contains(token.type) &&
!token.isIdentifier) {
reportSql(token, RegionType.keyword);
} else if (token is CommentToken) {
reportSql(token, RegionType.comment);
} else if (token is StringLiteralToken) {
reportSql(token, RegionType.string);
}
}
}
void reportSql(SyntacticEntity? entity, RegionType type) {
if (entity != null) {
report(HighlightRegion(
type, file.span(entity.firstPosition, entity.lastPosition)));
}
}
}
class _HighlightingVisitor extends RecursiveVisitor<_DriftHighlighter, void> {
@override
void visitCreateTriggerStatement(
CreateTriggerStatement e, _DriftHighlighter arg) {
arg.reportSql(e.triggerNameToken, RegionType.classTitle);
visitChildren(e, arg);
}
@override
void visitCreateViewStatement(CreateViewStatement e, _DriftHighlighter arg) {
arg.reportSql(e.viewNameToken, RegionType.classTitle);
visitChildren(e, arg);
}
@override
void visitColumnDefinition(ColumnDefinition e, _DriftHighlighter arg) {
arg
..reportSql(e.nameToken, RegionType.variable)
..reportSql(e.typeNames?.toSingleEntity, RegionType.type);
visitChildren(e, arg);
}
@override
void visitColumnConstraint(ColumnConstraint e, _DriftHighlighter arg) {
if (e is NotNull) {
arg.reportSql(e.$null, RegionType.keyword);
} else if (e is NullColumnConstraint) {
arg.reportSql(e.$null, RegionType.keyword);
}
super.visitColumnConstraint(e, arg);
}
@override
void visitNullLiteral(NullLiteral e, _DriftHighlighter arg) {
arg.reportSql(e, RegionType.builtIn);
}
@override
void visitNumericLiteral(Literal e, _DriftHighlighter arg) {
arg.reportSql(e, RegionType.number);
}
@override
void visitMoorSpecificNode(MoorSpecificNode e, _DriftHighlighter arg) {
if (e is DeclaredStatement) {
final name = e.identifier;
if (name is SimpleName) {
arg.reportSql(name.identifier, RegionType.functionTitle);
}
}
super.visitMoorSpecificNode(e, arg);
}
@override
void visitBooleanLiteral(BooleanLiteral e, _DriftHighlighter arg) {
arg.reportSql(e, RegionType.literal);
}
@override
void visitReference(Reference e, _DriftHighlighter arg) {
arg.reportSql(e, RegionType.variable);
}
@override
void visitTableReference(TableReference e, _DriftHighlighter arg) {
arg.reportSql(e.tableNameToken, RegionType.type);
}
@override
void visitTableInducingStatement(
TableInducingStatement e, _DriftHighlighter arg) {
arg.reportSql(e.tableNameToken, RegionType.classTitle);
if (e is CreateVirtualTableStatement) {
arg.reportSql(e.moduleNameToken, RegionType.invokedFunctionTitle);
}
visitChildren(e, arg);
}
@override
void visitVariable(Variable e, _DriftHighlighter arg) {
arg.reportSql(e, RegionType.variable);
}
}

View File

@ -1,9 +1,8 @@
//@dart=2.9
import 'package:drift_dev/src/analyzer/runner/file_graph.dart';
import 'package:drift_dev/src/analyzer/runner/results.dart';
extension CurrentResults on FoundFile {
ParsedMoorFile get parsedMoorOrNull {
ParsedMoorFile? get parsedMoorOrNull {
final result = currentResult;
if (result is ParsedMoorFile && isParsed) {
return result;