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

sqj.js driver #894

Merged
merged 22 commits into from
Nov 9, 2017
Merged

sqj.js driver #894

merged 22 commits into from
Nov 9, 2017

Conversation

daniel-lang
Copy link
Contributor

@daniel-lang daniel-lang commented Sep 16, 2017

A driver for sql.js based on the AbstractSqliteDriver.
Two things I'm not quiet sure about yet:
Naming
is there a better way than calling it SqljsDriver?

Import/Export
Since the database in sql.js is never persisted the library supports an import/export mechanism. The problem with that is, that the database to import has to passed to the constructor in createDatabaseConnection. Therefore the only option I could come up with was use an option field (databaseForImport) which can be set to new Uint8Array(0) if nothing should be imported.
And with

connection.driver as any;
driver.export();

the database can be exported. Is there a nicer way for the import and maybe even the export?

Tested in the browser, but should also work in node.js

closes #822

@pleerock
Copy link
Member

What if we implement following options:

  1. Automatically manage database saving for user. When he connects we read from a provided database (from connection options). Each time when he saves/inserts/removes/updates something we save database (how to save it defined in connection options too)

  2. Provide mechanizm for a user to manage saving by his own. For example:

connection.importDatabase(...); // or maybe call it loadDatabase or some better name

and

connection.exportDatabase(...); // or some better name

@pleerock
Copy link
Member

connection.driver as any;
driver.export();

Too dirty for users. I would like to avoid adding sqljs-specific methods to connection, however using "any" or any other casting isnt an option. Alternative is to make things like in mongo - create SqljsEntityManager (like MongoEntityManager) and add there two new methods for export and import. Also we'll need to create getSqljsManager method in index.ts Actually I like it more then what Ive suggested before to add it in connection (including the fact how often sqljs will be used its a very pragmatic and elegant solution).

is there a better way than calling it SqljsDriver

Probably yes. Alternative is to name it SqlJSDriver, but Im not sure if its better.

Tested in the browser, but should also work in node.js

we need to include it in ormconfig.dist / ormconfig.travis to run then in nodejs as well. Also probably we'll need to include tests enabled only for this driver which cover this specific functionality (option to auto save or option to provided place)

Therefore the only option I could come up with was use an option field (databaseForImport) which can be set to new Uint8Array(0) if nothing should be imported.

Im not sure I completely understood this part.... But we need to make things flexible and easy for users. For example user needs to be able to provide simply path, like "/test/db.sql" and it should be automatically handled (for nodejs it may save database in file system and for browser it can save it in local storage). What do you think about it, is it implementable? This does not exclude option to provide utf8array too.

@daniel-lang
Copy link
Contributor Author

Thanks for your feedback. I really like your ideas! I will see what I can do

@pleerock
Copy link
Member

pleerock commented Oct 6, 2017

@daniel-lang any updates on this PR?

@daniel-lang
Copy link
Contributor Author

I wasn't comfortable digging that deep into TypeORM, so I waited a bit to get more familiar with TypeORM. I hope, that I can update it this weekend or early next week so that it can be part of the 0.1 release

@daniel-lang
Copy link
Contributor Author

@pleerock I push the most recent ideas.
As you suggested I added a special entity manager so functions like loadDatabase(), saveDatabase() and exportDatabase() can be called easily.
For autosave, I thought it might be a good idea to use an interval and leave it to the user to decide how often he wants his database saved. There are two options autoSave (enable it) and autoSaveInterval to configure it.

This is still missing:

  • docs
  • tests

/**
* True if file or localStorage should be updated automatically
*/
readonly autoSave?: boolean;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe its better to do boolean|number and if its a number, its an interval?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What would be happen, when it's set to true? Then the interval would have to be a default value, right?

Copy link
Member

Choose a reason for hiding this comment

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

I think we can make autoSave to boolean|Function instead of two parameters

Copy link
Member

Choose a reason for hiding this comment

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

Or maybe even save: boolean|string|Function where string is a location, what do you think about it?

}
}
}

Copy link
Member

Choose a reason for hiding this comment

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

make those methods protected too and put comments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will add comments, but they are public because I have to call them from the entity manager

this.driver = driver;
this.connection = driver.connection;
}

Copy link
Member

Choose a reason for hiding this comment

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

public methods are separate block, please try to keep following common code convention

@@ -16,6 +18,9 @@ export class EntityManagerFactory {
if (connection.driver instanceof MongoDriver)
return new MongoEntityManager(connection);

// if (connection.driver instanceof SqljsDriver)
// return new SqljsEntityManager(connection, queryRunner);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to remove these lines becaus otherwise I get a TypeError: Object prototype may only be an Object or null: undefined error. Which, as far as I can tell, is because require('./EntityManager') inside SqljsEntityManager returns {}. But I don't really know why, the only information I found online was, that it could be a problem with circular dependency. But MongoEntityManager looks the same and works without problems

.travis.yml Outdated
@@ -12,6 +12,7 @@ services:
- docker

before_script:
- npm install sql.js
Copy link
Member

Choose a reason for hiding this comment

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

why? we don't do same for other drivers. You just need to install sql.js and save into dependencies

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right

@daniel-lang
Copy link
Contributor Author

Tests for load, export and save exist now.

How do you want to do the autoSave feature?

  • Like it is right know with two options (enable and interval)
  • Like you suggested with one option (what would be the interval if it's just set to true?)
  • Like you originally suggested, where it would save after every query

@pleerock
Copy link
Member

as for me interval does not make lot of sense. Database needs to be saved only after some modifications are applied on it, right? And usually modifications aren't so often, so saving database on that point kinda makes sense. Plus interval isn't reliable - in browser, what if interval is high (and making it low does not make sense because there are lot of reads and much less writes), what if user just closes the window and interval time isn't reached.

@pleerock
Copy link
Member

Also we'll need to create a good facade/interface for users to easily configure how it gonna autosave (where?)

@daniel-lang
Copy link
Contributor Author

Okay, I will rewrite the autosave part to save after insert, update and delete.

Also we'll need to create a good facade/interface for users to easily configure how it gonna autosave

What do you want the user to configure other than enable it and the location (both of them have an option)?

@pleerock
Copy link
Member

What do you want the user to configure other than enable it and the location (both of them have an option)?

Do you mean location like "/test/db.sql" ? If yes, then can we implement it? Should be yes in nodejs, but what about browser? Shall we store it someone in the local storage? What about if user wants to save using his own method, how do he configure it?

@daniel-lang
Copy link
Contributor Author

The way it's implemented right now is:

  • location option
    • nodejs: specify a file location like "/test/db.sql"
    • browser: specify the localstorage key to use (used like localstorage.setItem(location, database))
  • users can call SqljsEntityManager.exportDatabase() at any time and get the current database, but defining your one method that gets called on autosave is not support right now. But I like the idea

@pleerock
Copy link
Member

Ah, nice, its implemented already. Then what are things left to be done?:

  • defining your one method that gets called on autosave

@pleerock
Copy link
Member

Also, does it have any breaking change? If yes, then PR must be against 0.2.0 branch, if no, then we can merge it directly into master and release this change in 0.1.1

@@ -128,6 +129,13 @@ export class Connection {
return this.manager as MongoEntityManager;
}

get sqljsManager(): SqljsEntityManager {
Copy link
Member

Choose a reason for hiding this comment

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

missing comment block

readonly type: "sqljs";

/**
* The database definition and data to import
Copy link
Member

Choose a reason for hiding this comment

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

Don't forget dots after sentence 🙈

/**
* Sqlite-specific connection options.
*/
export interface SqljsConnectionOptions extends BaseConnectionOptions {
Copy link
Member

Choose a reason for hiding this comment

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

I would improve all comments in this file - make them more concrete and simple for users understanding

readonly autoSaveCallback?: Function;

/**
* file path or local storage key to load and save database
Copy link
Member

Choose a reason for hiding this comment

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

Captal + dot

readonly database?: Uint8Array;

/**
* True if file or localStorage should be updated automatically
Copy link
Member

Choose a reason for hiding this comment

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

Bad comment

@@ -0,0 +1,188 @@
import { AbstractSqliteDriver } from "../sqlite-abstract/AbstractSqliteDriver";
Copy link
Member

Choose a reason for hiding this comment

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

Please follow same code conventions everywhere - comment on each property, class, method, dots, etc.

}

if (query.toUpperCase().match(/(COMMIT)/)) {
this.driver.autoSave();
Copy link
Member

Choose a reason for hiding this comment

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

what if autoSave is an async operation? Callback provided by a user most probably should be a promise since it'll most probably save in the cloud / filesystem / database using async api

Copy link
Member

Choose a reason for hiding this comment

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

also this code looks a bit like a monkey patch. Isn't it better to do it after save method call? Plus after operations called by insert/update/delete query builders?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's a bit of a monkey patch because I didn't want to save after every update/insert/delete, because they are always executed in a transaction, right? So I guess the best place would be commitTransaction().

what if autoSave is an async operation?

The question is, do we have to wait for the save operation to complete? What would change?

* that are unique to Sql.js
*/
export class SqljsEntityManager extends EntityManager {
private driver: SqljsDriver;
Copy link
Member

Choose a reason for hiding this comment

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

use code separation blocks

/**
* Gets a sql.js specific Entity Manager that allows to perform special load and save operations
*
* Available only in connection with the sqljs driver
Copy link
Member

Choose a reason for hiding this comment

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

doooots 😭

@daniel-lang
Copy link
Contributor Author

The driver is tested in the browser

}
];

repository.createQueryBuilder("api").insert().into(Post).values(posts).execute();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only problem left is the autoSave detection. Right now autoSave is called when a transaction is committed or insert(), update() or delete() in the query runner is called and no transaction is active.
But this will not detect bulk inserts like this one as they are executed using the query() method from query runner.
Any ideas?

Copy link
Member

Choose a reason for hiding this comment

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

maybe for now you simply call autoSave method of the driver in the case if dirver is sqlitejs right in the insert/update/delete query builders? Now Im not sure about better solution

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know if this is that much better than my original "monkey patch" where I checked if the query contains a INSERT, UPDATE or DELETE

/**
* Creates an Entity Manager for the current connection with the help of the EntityManagerFactory.
*/
protected createEntityManager(queryRunner?: QueryRunner): EntityManager {
createEntityManager(queryRunner?: QueryRunner): EntityManager {
Copy link
Member

Choose a reason for hiding this comment

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

what is the purpose of this method being public? I don't really want to confuse users with both getEntityManager and createEntityManager. Plus there is a createQueryRunner which creates an entity as well...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this method to resolve a circlular dependency. And the EntityManager also uses the method...

const content = JSON.stringify(this.databaseConnection.export());
PlatformTools.getGlobalVariable().localStorage.setItem(path, content);
const database: Uint8Array = this.databaseConnection.export();
const databaseArray = [].slice.call(database);
Copy link
Member

Choose a reason for hiding this comment

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

what exact [].slice.call(database) does? copies array? Please add comment for dummies like me who dont know what this specific code does

*/
async commitTransaction(): Promise<void> {
await super.commitTransaction();
this.driver.autoSave();
Copy link
Member

Choose a reason for hiding this comment

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

did you miss comment where I told that autoSave most probably should be a promise?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No I didn't.
But I added back then: What would be the difference? I don't know if it would be a good idea to wait for the save operation to complete since it will slow down the driver.

Copy link
Member

Choose a reason for hiding this comment

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

it will be correct to wait a save operation to complete, otherwise users may face unexpected behaviour

Copy link
Member

@pleerock pleerock Nov 7, 2017

Choose a reason for hiding this comment

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

Plus if there will be an error they will see it, it won't disappear in the air

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed that, to wait for save

/**
* Creates an Entity Manager for the current connection with the help of the EntityManagerFactory.
*/
createEntityManager(queryRunner?: QueryRunner): EntityManager {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only problem left....I don't know a way around making it public, because I need it in EntityManager to remove the dependency to EntityManagerFactory which again depends on EntityManager. I don't know why this error appeared now, after a added a second special EntityManager...

Copy link
Member

Choose a reason for hiding this comment

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

Okay, lets left it then

…s test as it is already included in other tests
@pleerock
Copy link
Member

pleerock commented Nov 8, 2017

Are we done with this PR and it can be merged?

@daniel-lang
Copy link
Contributor Author

I think so!

@pleerock pleerock merged commit 4d7ea31 into typeorm:master Nov 9, 2017
@pleerock
Copy link
Member

pleerock commented Nov 9, 2017

Okay, I merged it. Can you please merge master brunch into next branch now? Next branch is 0.2.0.alpha and you can install it via npm i typeorm@next. Ive completely refactored persistence mechanizm (and still working on it). Conflicts shall be in "update"/"insert"/"delete" methods - they all removed in favour of query builder functionality. Logic of taking "last insert id" is moved into createGeneratedMap method

@makamekm
Copy link

makamekm commented Nov 10, 2017

Thank you very much! I'm gonna donate you.

@daniel-lang daniel-lang deleted the sqljs branch December 19, 2017 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

sql.js
3 participants