Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate sqlparser #361

Closed
wants to merge 39 commits into from
Closed

Integrate sqlparser #361

wants to merge 39 commits into from

Conversation

mqus
Copy link
Collaborator

@mqus mqus commented Jun 15, 2020

I have now finished an early working version of the integrated sqlparser. There are still some //todos open, most of them related to the typeConverters, because I assume that this PR will not be merged before #318 (though mostly because the query parser is not quite finished).

Features :

  • An even more intelligent generator overall and the foundation for more features
  • Query validation for @DatabaseView and @Query. (Provide more query validation #58)
  • Type derivation and matching for @DatabaseView and @Query.
  • Dependency and "What entities are affected" derivation, including foreign keys of entities
  • Better parameter handling
  • Outputting simple values in Queries (Impossible to return int value from COUNT(*) #200)
  • Cheaper streams on DatabaseViews

TODO:

Implement some Features/Refactor

  • Mapper for primitive types
  • Return type checker
  • Restructure AnalyzeResult creation
  • Maybe separate type checker from view converter
  • Check view query for variables
  • Map bool type to int with typehint in converters
  • Make listInsertionPositions a (sorted) list with dedicated type
  • Formulate missing errors
  • Check for and add missing errors

Cleanup

  • Check todo comments for missing functionality and expand this todo list
  • Add @nonNull/@nullable annotations to new functions and check usage
  • Add Docstrings to crucial functions

Integration of other PRs

Fixing existing tests

  • integration tests (pretty much already worked without any changes)
  • queryadapter unit tests
  • generator unit tests

Adding new tests

Update README

  • Document subtleties, such as executing a query only up to the first semicolon and not allowing more than 1000 parameter values (both limitations of the sqlite implementations of e.g. android)
  • Update Documentation regarding streams on Views

Small Guide to the PR

I used the library sqlparser from https://github.com/simolus3/moor, like mentioned before.
I added a new subdirectory to processor called query_analyzer. There are multiple files in there:

  • engine.dart/AnalyzerEngine is the functional wrapper for the sqlparser library, which is initialized once per database and holds the context for the analyzer. It stores dependencies, maps database names to Queryables and is able to analyze queries by wrapping sqlparsers engine. All entities and database views have to be registered here, with database views having to come after entities for a successful validation.

  • dependency_graph.dart is a small utility data structure for the engine to hold the dependency graph. It currently calculates the indirect dependencies without caching any results to keep it simple. There are no plans to include caching yet as generator performance is not a priority right now.

  • type_checker.dart contains functions to compare the types of view fields/output fields of a query output to the derived youtput from the associated query and reports errors.

  • visitors.dart Is a visitor which walks the sqlparser tree and stores all found variable references, such as ?, ?44, :varname, and so on. It is used by the QueryProcessor to first validate the parameters and then find and replace all named variables starting with a colon in the sql string with either a numbered variable (for normal parameters of the query) or a placeholder (for list parameters).

I also added query.dart and query_processor.dart, which are responsible for holding and processing the query, including the derivation of list parameter mappings, output types, dependencies and affected entities, as well as the validation of the query.

Finally, I also added a QueryMethodReturnType to the value_object folder to simplify the access to the return type description of query methods and restructured the query_method_writer.dart.

If there are any questions or suggestions, I'm happy to listen, while I work on the tasks written above.

Closes #321

@mqus mqus added the feature Request and implementation of a feature (release drafter) label Jun 15, 2020
@codecov
Copy link

codecov bot commented Jun 15, 2020

Codecov Report

Merging #361 into develop will increase coverage by 3.13%.
The diff coverage is 90.00%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #361      +/-   ##
===========================================
+ Coverage    81.55%   84.68%   +3.13%     
===========================================
  Files           60       10      -50     
  Lines         1534      209    -1325     
===========================================
- Hits          1251      177    -1074     
+ Misses         283       32     -251     
Flag Coverage Δ
#floor 84.68% <90.00%> (+3.13%) ⬆️
#floor_generator ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
floor/lib/src/adapter/query_adapter.dart 95.23% <90.00%> (+2.55%) ⬆️
floor_generator/lib/value_object/index.dart
...or_generator/lib/value_object/deletion_method.dart
...loor_generator/lib/value_object/change_method.dart
...enerator/lib/misc/change_method_writer_helper.dart
.../lib/processor/error/database_processor_error.dart
floor_generator/lib/misc/foreign_key_action.dart
..._generator/lib/writer/database_builder_writer.dart
...enerator/lib/writer/transaction_method_writer.dart
...lib/processor/error/queryable_processor_error.dart
... and 16 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7888578...6ac8111. Read the comment docs.

@mqus mqus mentioned this pull request Jun 15, 2020
9 tasks
@mqus mqus changed the title Add a queryparser Integrate sqlparser Jun 15, 2020
@vitusortner
Copy link
Collaborator

This looks very promising 👏 I'll make time for reviewing the changes over the next days. If there is anything in particular that I can help you with, just let me know!

@mqus mqus linked an issue Jun 16, 2020 that may be closed by this pull request
@mqus
Copy link
Collaborator Author

mqus commented Jun 19, 2020

In the meantime, let me give you a small taste of what we're getting with @simolus3's sqlparser. I currently fix the existing tests on the query_method writer and processor and many of the tests have some arbitrary queries in them, that don't reference any existing entities or wrong names, which weren't necessary to be right until now. This is the kind of error messages I get:

The query contained errors: Could not find Persons. Available are: sqlite_master, sqlite_sequence, Person, Name
package:_resolve_source/_resolve_source.dart:8:28
  ╷
8 │       Future<List<Person>> findPersonsWithNamesLike(String name);
  │                            ^^^^^^^^^^^^^^^^^^^^^^^^
  ╵

I think they're awesome. I'll look into also printing the line before that which actually contains the query.
EDIT: I actually didn't completely bring out the potential here, as most of sqlparsers errors come with a highlighted span which also shows you where you made the mistake. This looks like darts highlighting above, just with sql :)

@mqus
Copy link
Collaborator Author

mqus commented Jun 19, 2020

If there is anything in particular that I can help you with, just let me know!

A list of testcases you would like to see covered, I have my own list but more is better :)

@vitusortner
Copy link
Collaborator

In the meantime, let me give you a small taste of what we're getting with @simolus3's sqlparser. I currently fix the existing tests on the query_method writer and processor and many of the tests have some arbitrary queries in them, that don't reference any existing entities or wrong names, which weren't necessary to be right until now. This is the kind of error messages I get:

The query contained errors: Could not find Persons. Available are: sqlite_master, sqlite_sequence, Person, Name
package:_resolve_source/_resolve_source.dart:8:28
  ╷
8 │       Future<List<Person>> findPersonsWithNamesLike(String name);
  │                            ^^^^^^^^^^^^^^^^^^^^^^^^
  ╵

I think they're awesome. I'll look into also printing the line before that which actually contains the query.

That's amazing! The capabilities your changes provide to the project are more than great. Good job 👏

@mqus mqus linked an issue Jul 27, 2020 that may be closed by this pull request
@vitusortner
Copy link
Collaborator

I'll look into it ASAP!

Copy link
Collaborator

@vitusortner vitusortner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll deliver the review in parts. Here's the first part 🙌

Comment on lines +419 to +420
Your SQL queries will be validated completely while generating the code.
These queries have to return either a `Future` or a `Stream` of an entity, a view, a primitive value or `void`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ The query parser, in the future, will allows us to parse @Query results into non-primitive values, right? #94

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need another PR but it should be as simple as adding another Queryable subtype and using the same processing as for entities/views. The type checking and the mapping already exists.

streamController.add(otherEntity);

await Future<void>.delayed(const Duration(milliseconds: 100));
await streamController.close();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 We can remove this line as we close StreamControllers in the tearDown block after each test.

Suggested change
await streamController.close();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove that, the test will fail because of a deadlock/timeout, since the emitsInOrder still waits for the Stream to complete, but the Stream only gets closed after the test succeeded.

floor/test/integration/boolean_conversions/bool_test.dart Outdated Show resolved Hide resolved
Comment on lines +42 to +54
await Future<void>.delayed(const Duration(milliseconds: 100));
await caseDao.updateName();

await Future<void>.delayed(const Duration(milliseconds: 100));
await caseDao.insertPerson(person1);

await Future<void>.delayed(const Duration(milliseconds: 100));
await caseDao.updateName();

await Future<void>.delayed(const Duration(milliseconds: 100));
await caseDao.insertPerson(person2);

await Future<void>.delayed(const Duration(milliseconds: 100));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ I guess without these delays, the tests don't run?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they will not complete successfully and will have issues like the following:

  Expected: should do the following in order:
            • emit an event that []
            • emit an event that []
            • emit an event that [Person:Person{id: 1, name: Baba}]
            • emit an event that [Person:Person{id: 1, name: Baba}]
            • emit an event that [Person:Person{id: 1, name: Baba}, Person:Person{id: 2, name: Me}]
            • emit an event that [Person:Person{id: 1, name: Baba}, Person:Person{id: 2, name: Me}]
    Actual: <Instance of '_BroadcastStream<List<Person>>'>
     Which: emitted • []
                    • [Person{id: 1, name: Baba}]
              which didn't emit an event that [] because it emitted an event that longer than expected at location [0]

Because the first two (or three) statements were completed together before the stream got to re-query the database.

final list = ['a', 'b', 'c', 'd', 'e', 'f', null];

final aInList = await deepDao.isXinList('a', list);
expect(aInList, equals(true));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ The SQLite database returns "a" in this case? Which use cases do you see for mapping a result (of not INTEGER) to true when the return type is bool ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my case it returns 1 and not "a". This is also what I want to test: a simple function that does not refer to any table and returns a primitive boolean (or null) value.

sqlite> SELECT "a" IN ("a", "2", "s");
1

isXinList is just a complicated way to write ['a', 'b', 'c', 'd', 'e', 'f', null].contains('a').

Comment on lines 139 to +144
[], // initial state,
[], // after inserting person1,
[], // after inserting person2,
[dog1], // after inserting dog1
[dog1], // after inserting dog2
//[], // after removing person1. Does not work because
// ForeignKey-relations are not considered yet (#321)
[], // after removing person1.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ Good job! This is great!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I found out that this is still suboptimal, since it will be also triggered if a person is inserted, even if that will never change dog. 😆

This is what I meant in #373 .

String decapitalize() {
return '${this[0].toLowerCase()}${substring(1)}';
}

/// Makes the first letter of the supplied string [value] lowercase.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Makes the first letter of the supplied string [value] lowercase.
/// Makes the first letter of the supplied string [value] uppercase.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix!

floor_generator/lib/misc/type_utils.dart Show resolved Hide resolved
Comment on lines +69 to +74
const sqlToBasicType = {
SqlType.blob: BasicType.blob,
SqlType.integer: BasicType.int,
SqlType.real: BasicType.real,
SqlType.text: BasicType.text,
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Judging from the usages of this map, we could transform it into an extension function on SqlType (String).

Suggested change
const sqlToBasicType = {
SqlType.blob: BasicType.blob,
SqlType.integer: BasicType.int,
SqlType.real: BasicType.real,
SqlType.text: BasicType.text,
};
extension SqlTypeExtension on String {
BasicType toBasicType() {
switch (this) {
case SqlType.blob:
return BasicType.blob;
...
}
}
}```

@@ -0,0 +1,76 @@
import 'package:floor_generator/misc/annotations.dart';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 We could rename this file to match the class it's holding. engine.dart -> analyzer_engine.dart

import 'package:floor_generator/value_object/view.dart' as floor;
import 'package:sqlparser/sqlparser.dart' hide Queryable;

const String varlistPlaceholder = ':varlist';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 There is no need to explicitly define the type here as it can be inferred.

Suggested change
const String varlistPlaceholder = ':varlist';
cons varlistPlaceholder = ':varlist';

floor_generator/lib/processor/query_analyzer/engine.dart Outdated Show resolved Hide resolved
floor_generator/lib/processor/query_analyzer/engine.dart Outdated Show resolved Hide resolved
floor_generator/lib/processor/query_analyzer/engine.dart Outdated Show resolved Hide resolved
floor_generator/lib/processor/query_analyzer/engine.dart Outdated Show resolved Hide resolved
Comment on lines 35 to 46
inner.registerTable(_convertEntityToTable(entity));

registry[entity.name] = entity;

//register dependencies
final directDependencies = entity.foreignKeys
.where((e) => e.canChangeChild)
.map((e) => e.parentName);
dependencies.add(entity.name, directDependencies);
}

void registerView(floor.View floorView, View convertedView) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Entities are converted in the analyzer engine but views are converted in the view processor. Do you see a way to either have both in the analyzer engine or both in the appropriate processor?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this and while entities can be converted without any fuzz (they don't have to be analyzed and can't throw any errors), Views have their query checked, have to be typechecked, can throw errors (which need the source reference). I had both solutions in the code at some time:

  1. Add all entities and views to the engine at the DatabaseProcessor-level, after processing them.
    This has the "downside" that the conversion of the views is just treated like a conversion for the engine, while it really is the second part of the processing, throwing errors and determining dependencies.
  2. Add views and entities to the engine, as the last processing step inside their process() methods.
    This solved the view issues in 1., but introduced the engine to the entity processor, which didn't really need it. It also made the tests for entities a lot more complex for gaining nothing in return.

So I chose to really handle them differently. This has it's own issues, like also having to treat them differently in tests, but I think that the tradeoffs are ok. Of course, this is just my opinion and I'm also not that sure on what the right way is. If you have a clear preference I'll gladly change this.

@mqus
Copy link
Collaborator Author

mqus commented Aug 16, 2020

pub run test_cov is failing, probably due to flutter/flutter#57864

@mqus
Copy link
Collaborator Author

mqus commented Feb 11, 2021

I think that there are enough conflicts by now that this has to be rewritten from scratch.

I'm currently not up for it but if I have some advice for people who will try this in the future: take smaller steps. I rewrote some logic and also added a parser which already did 90% of the things it can do, all in one PR. This is too much to be reviewed reasonably, which is another reason why I think this PR has no hope of getting merged as-is, even when fixing all conflicts and updating it.

For those reasons, I will close this PR. Maybe I will have some time to go back and try again but currently I don't. I will gladly to give advice and reviews for future attempts of contributors, just ping me ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Request and implementation of a feature (release drafter)
3 participants