-
Notifications
You must be signed in to change notification settings - Fork 194
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
Integrate sqlparser #361
Conversation
Codecov Report
@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report at Codecov.
|
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! |
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:
I think they're awesome. I'll look into also printing the line before that which actually contains the query. |
A list of testcases you would like to see covered, I have my own list but more is better :) |
That's amazing! The capabilities your changes provide to the project are more than great. Good job 👏 |
…ve return types in dao methods
I'll look into it ASAP! |
There was a problem hiding this 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 🙌
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`. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 StreamController
s in the tearDown
block after each test.
await streamController.close(); |
There was a problem hiding this comment.
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.
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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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')
.
[], // 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. |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Makes the first letter of the supplied string [value] lowercase. | |
/// Makes the first letter of the supplied string [value] uppercase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will fix!
const sqlToBasicType = { | ||
SqlType.blob: BasicType.blob, | ||
SqlType.integer: BasicType.int, | ||
SqlType.real: BasicType.real, | ||
SqlType.text: BasicType.text, | ||
}; |
There was a problem hiding this comment.
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
).
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'; |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
const String varlistPlaceholder = ':varlist'; | |
cons varlistPlaceholder = ':varlist'; |
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- 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. - 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.
|
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 ;) |
I have now finished an early working version of the integrated sqlparser. There are still some
//todo
s 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 :
@DatabaseView
and@Query
. (Provide more query validation #58)@DatabaseView
and@Query
.TODO:
Implement some Features/Refactor
Cleanup
@nonNull
/@nullable
annotations to new functions and check usageIntegration of other PRs
Fixing existing tests
Adding new tests
Update README
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
calledquery_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 thequery_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