From 8376a78fedfa95cb0987d9165d6cdde5f8bb4ee2 Mon Sep 17 00:00:00 2001 From: pvincent Date: Sun, 20 Sep 2020 18:22:51 +0400 Subject: [PATCH] first commit --- .gitignore | 5 + .travis.yml | 28 + LICENSE | 21 + README.md | 357 +++++++++ haxelib.json | 11 + src/sys/db/Manager.hx | 723 ++++++++++++++++++ src/sys/db/Object.hx | 76 ++ src/sys/db/RecordInfos.hx | 85 +++ src/sys/db/RecordMacros.hx | 1441 ++++++++++++++++++++++++++++++++++++ src/sys/db/TableCreate.hx | 109 +++ src/sys/db/Transaction.hx | 70 ++ src/sys/db/Types.hx | 127 ++++ test.hxml | 5 + test/Main.hx | 21 + test/MySQLTest.hx | 588 +++++++++++++++ test/MySpodClass.hx | 126 ++++ test/SQLiteTest.hx | 569 ++++++++++++++ testPHP.hxml | 8 + 18 files changed, 4370 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 haxelib.json create mode 100644 src/sys/db/Manager.hx create mode 100644 src/sys/db/Object.hx create mode 100644 src/sys/db/RecordInfos.hx create mode 100644 src/sys/db/RecordMacros.hx create mode 100644 src/sys/db/TableCreate.hx create mode 100644 src/sys/db/Transaction.hx create mode 100644 src/sys/db/Types.hx create mode 100644 test.hxml create mode 100644 test/Main.hx create mode 100644 test/MySQLTest.hx create mode 100644 test/MySpodClass.hx create mode 100644 test/SQLiteTest.hx create mode 100644 testPHP.hxml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a4d0d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/test.n +/test.sqlite +/.vscode +lib/* +/index.php diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c0faa9c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: haxe +sudo: true + +haxe: +- development +- 3.4.7 +- 3.4.2 + +before_install: +#install php 7.2 on ubuntu trusty +- sudo add-apt-repository ppa:ondrej/php -y +- sudo apt-get update +- sudo apt-get install -y php7.2 +#create database in mysql5.6 +- mysql -e 'CREATE DATABASE IF NOT EXISTS test;' + +services: + - mysql + +install: +- haxelib install all --always + +script: +- haxe test.hxml +- neko test.n mysql://travis:@127.0.0.1/test +- php -v +- haxe testPHP.hxml +- php index.php mysql://travis:@127.0.0.1/test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ec0d92 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Haxe Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dea27de --- /dev/null +++ b/README.md @@ -0,0 +1,357 @@ +[![Build Status](https://travis-ci.org/HaxeFoundation/record-macros.svg?branch=master)](https://travis-ci.org/HaxeFoundation/record-macros) + +Record macros is a macro-based library that provides object-relational mapping to Haxe. +With `record-macros`, you can define some Classes that will map to your database tables. You can then manipulate tables like objects, by simply modifying the table fields and calling a method to update the datas or delete the entry. For most of the standard stuff, you only need to provide some basic declarations and you don't have to write one single SQL statement. You can later extend `record-macros` by adding your own SQL requests for some application-specific stuff. + +## Creating a Record +You can simply declare a `record-macros` Object by extending the sys.db.Object class : + +```haxe +import sys.db.Types; + +class User extends sys.db.Object { + public var id : SId; + public var name : SString<32>; + public var birthday : SDate; + public var phoneNumber : SNull; +} +``` +As you can see in this example, we are using special types declared in sys.db.Types in order to provide additional information for `record-macros`. Here's the list of supported types : + + * `Null, SNull` : tells that this field can be NULL in the database + * `Int, SInt` : a classic 32 bits signed integer (SQL INT) + * `Float, SFloat` : a double precision float value (SQL DOUBLE) + * `Bool, SBool` : a boolean value (SQL TINYINT(1) or BOOL) + * `Date, SDateTime` : a complete date value (SQL DATETIME) + * `SDate` : a date-only value (SQL DATE) + * `SString` : a size-limited string value (SQL VARCHAR(K)) + * `String, SText` : a text up to 16 MB (SQL MEDIUMTEXT) + * `SBytes` : a fixed-size bytes value (SQL BINARY(K)) + * `SBinary, haxe.io.Bytes` : up to 16 MB bytes (SQL MEDIUMBLOB) + * `SId` : same as SInt but used as an unique ID with auto increment (SQL INT AUTO INCREMENT) + * `SEnum` : a single enum without parameters which index is stored as a small integer (SQL TINYINT UNSIGNED) + * `SFlags` : a 32 bits flag that uses an enum as bit markers. See EnumFlags + * `SData` : allow arbitrary serialized data (see below) + +### Advanced Types + +The following advanced types are also available if you want a more custom storage size : + + * `SUInt` : an unsigned 32 bits integer (SQL UNSIGNED INT) + * `STinyInt / STinyUInt` : a small 8 bits signed/unsigned integer (SQL TINYINT) + * `SSmallInt / SSmallUInt` : a small 16 bits signed/unsigned integer (SQL SMALLINT) + * `SMediumIInt / SMediumUInt` : a small 24 bits signed/unsigned integer (SQL MEDIUMINT) + * `SBigInt` : a 64 bits signed integer (SQL BIGINT) - typed as Float in Haxe + * `SSingle` : a single precision float value (SQL FLOAT) + * `STinyText` : a text up to 255 bytes (SQL TINYTEXT) + * `SSmallText` : a text up to 65KB (SQL TEXT) + * `STimeStamp` : a 32-bits date timestamp (SQL TIMESTAMP) + * `SSmallBinary` : up to 65 KB bytes (SQL BLOB) + * `SLongBinary` : up to 4GB bytes (SQL LONGBLOB) + * `SUId` : same as SUInt but used as an unique ID with auto increment (SQL INT UNSIGNED AUTO INCREMENT) + * `SBigId` : same as SBigInt but used as an unique ID with auto increment (SQL BIGINT AUTO INCREMENT) - compiled as Float in Haxe + * `SSmallFlags` : similar to SFlags except that the integer used to store the data is based on the number of flags allowed + +## Metadata +You can add Metadata to your `record-macros` class to declare additional informations that will be used by `record-macros`. + +Before each class field : + + * `@:skip` : ignore this field, which will not be part of the database schema + * `@:relation` : declare this field as a relation (see specific section below) + +Before the `record-macros` class : + + * `@:table("myTableName")` : change the table name (by default it's the same as the class name) + * `@:id(field1,field2,...)` : specify the primary key fields for this table. For instance the following class does not have a unique id with auto increment, but a two-fields unique primary key : + +```haxe +@:id(uid,gid) +class UserGroup extends sys.db.Object { + public var uid : SInt; + public var gid : SInt; +} +``` + + * `@:index(field1,field2,...,[unique])` : declare an index consisting of the specified classes fields - in that order. If the last field is unique then it means that's an unique index (each combination of fields values can only occur once) + + +## Init/Cleanup +There are two static methods that you might need to call before/after using `record-macros` : + + * `sys.db.Manager.initialize()` : will initialize the created managers. Make sure to call it at least once before using `record-macros`. + * `sys.db.Manager.cleanup()` : will cleanup the temporary object cache. This can be done if you are using server module caching to free memory or after a rollback to make sure that we don't use the cached object version. + +## Creating the Table +After you have declared your table you can create it directly from code without writing SQL. All you need is to connect to your database, for instance by using sys.db.Mysql, then calling sys.db.TableCreate.create that will execute the CREATE TABLE SQL request based on the `record-macros` infos : + +```haxe +var cnx = sys.db.Mysql.connect({ + host : "localhost", + port : null, + user : "root", + pass : "", + database : "testBase", + socket : null, +}); +sys.db.Manager.cnx = cnx; +if ( !sys.db.TableCreate.exists(User.manager) ) +{ + sys.db.TableCreate.create(User.manager); +} +``` + +Please note that currently TableCreate will not create the index or initialize the relations of your table. + +## Insert +In order to insert a new `record-macros`, you can simply do the following : + +```haxe +var u = new User(); +u.name = "Random156"; +u.birthday = Date.now(); +u.insert(); +``` +After the `.insert()` is done, the auto increment unique id will be set and all fields that were null but not declared as nullable will be set to their default value (0 for numbers, "" for strings and empty bytes for binaries) + +## Manager +Each `record-macros` object need its own manager. You can create your own manager by adding the following line to your `record-macros` class body : + +```haxe +public static var manager = new sys.db.Manager(User); +``` +However, the `record-macros` Macros will do it automatically for you, so only add this if you want create your own custom Manager which will extend the default one. + +## Get +In order to retrieve an instance of your `record-macros`, you can call the manager get method by using the object unique identifier (primary key) : + +```haxe +var u = User.manager.get(1); +if( u == null ) throw "User #1 not found"; +trace(u.name); +``` +If you have a primary key with multiple values, you can use the following declaration : + +```haxe +var ug = UserGroup.manager.get({ uid : 1, gid : 2 }); +// ... +``` + +## Update/Delete +Once you have an instance of your `record-macros` object, you can modify its fields and call .update() to send these changes to the database : + +```haxe +var u = User.manager.get(1); +if( u.phoneNumber == null ) u.phoneNumber = "+3360000000"; +u.update(); +``` +You can also use `.delete()` to delete this object from the database : + +```haxe +var u = User.manager.get(1); +if( u != null ) u.delete(); +``` + +## Search Queries +If you want to search for some objects, you can use the `.manager.search` method : + +```haxe +var minId = 10; +for( u in User.manager.search($id < minId) ) { + trace(u); +} +``` +In order to differentiate between the database fields and the Haxe variables, all the database fields are prefixed with a dollar in search queries. + +Search queries are checked at compiletime and the following SQL code is generated instead : + +```haxe +unsafeSearch("SELECT * FROM User WHERE id < "+Manager.quoteInt(minId)); +``` +The code generator also makes sure that no SQL injection is ever possible. + +## Syntax +The following syntax is supported : + + * constants : integers, floats, strings, null, true and false + * all operations `+, -, *, /, %, |, &, ^, >>, <<, >>>` + * unary operations `!, - and ~` + * all comparisons : `== , >= , <=, >, <, !=` + * bool tests : `&& , ||` + * parenthesis + * calls and fields accesses (compiled as Haxe expressions) + +When comparing two values with == or != and when one of them can be NULL, the SQL generator is using the <=> SQL operator to ensure that NULL == NULL returns true and NULL != NULL returns false. + +## Additional Syntax +It is also possible to use anonymous objects to match exact values for some fields (similar to previous `record-macros` but typed : + +```haxe +User.manager.search({ id : 1, name : "Nicolas" }) +// same as : +User.manager.search($id == 1 && $name == "Nicolas") +// same as : +User.manager.search($id == 1 && { name : "Nicolas" }) +``` + +You can also use if conditions to generate different SQL based on Haxe variables (you cannot use database fields in if test) : + +```haxe +function listName( ?name : String ) { + return User.manager.search($id < 10 && if( name == null ) true else $name == name); +} +``` + +## SQL operations +You can use the following SQL global functions in search queries : + + * `$now() : SDateTime`, returns the current datetime (SQL NOW()) + * `$curDate() : SDate`, returns the current date (SQL CURDATE()) + * `$date(v:SDateTime) : SDate`, returns the date part of the DateTime (SQL DATE()) + * `$seconds(v:Float) : SInterval`, returns the date interval in seconds (SQL INTERVAL v SECOND) + * `$minutes(v:Float) : SInterval`, returns the date interval in minutes (SQL INTERVAL v MINUTE) + * `$hours(v:Float) : SInterval`, returns the date interval in hours (SQL INTERVAL v HOUR) + * `$days(v:Float) : SInterval`, returns the date interval in days (SQL INTERVAL v DAY) + * `$months(v:Float) : SInterval`, returns the date interval in months (SQL INTERVAL v MONTH) + * `$years(v:Float) : SInterval`, returns the date interval in years (SQL INTERVAL v YEAR) + +You can use the following SQL operators in search queries : + + * `stringA.like(stringB)` : will use the SQL LIKE operator to find if stringB if contained into stringA + +## SQL IN +You can also use the Haxe in operator to get similar effect as SQL IN : + +```haxe +User.manager.search($name in ["a","b","c"]); +``` +You can pass any Iterable to the in operator. An empty iterable will emit a false statement to prevent sql errors when doing IN (). + +## Search Options +After the search query, you can specify some search options : + +```haxe +// retrieve the first 20 users ordered by ascending name +User.manager.search(true,{ orderBy : name, limit : 20 }); +``` + +The following options are supported : + + * `orderBy` : you can specify one of several order database fields and use a minus operation in front of the field to indicate that you want to sort in descending order. For instance orderBy : [-name,id] will generate SQL ORDER BY name DESC, id + * `limit` : specify which result range you want to obtain. You can use Haxe variables and expressions in limit values, for instance : { limit : [pos,length] } + * `forceIndex` : specify that you want to force this search to use the specific index. For example to force a two-fields index use { forceIndex : [name,date] }. The index name used in that case will be TableName_name_date + +## Select/Count/Delete +Instead of search you can use the `manager.select` method, which will only return the first result object : + +```haxe +var u = User.manager.select($name == "John"); +// ... +``` +You can also use the manager.count method to count the number of objects matching the given search query : + +```haxe +var n = User.manager.count($name.like("J%") && $phoneNumber != null); +// ... +``` +You can delete all objects matching the given query : + +```haxe +User.manager.delete($id > 1000); +``` + +## Relations +You can declare relations between your database classes by using the @:relation metadata : + +```haxe +class User extends sys.db.Object { + public var id : SId; + // .... +} +class Group extends sys.db.Object { + public var id : SId; + // ... +} + +@:id(gid,uid) +class UserGroup extends sys.db.Object { + @:relation(uid) public var user : User; + @:relation(gid) public var group : Group; +} +``` +The first time you read the user field from an UserGroup instance, `record-macros` will fetch the User instance corresponding to the current uid value and cache it. If you set the user field, it will modify the uid value as the same time. + +## Locking +When using transactions, the default behavior for relations is that they are not locked. You can make there that the row is locked (SQL SELECT...FOR UPDATE) by adding the lock keyword after the relation key : + +```haxe +@:relation(uid,lock) public var user : User; +``` + +## Cascading +Relations can be strongly enforced by using CONSTRAINT/FOREIGN KEY with MySQL/InnoDB. This way when an User instance is deleted, all the corresponding UserGroup for the given user will be deleted as well. + +However if the relation field can be nullable, the value will be set to NULL. + +If you want to enforce cascading for nullable-field relations, you can add the cascade keyword after the relation key : + +```haxe + @:relation(uid,cascade) var user : Null; +``` + +## Relation Search +You can search a given relation by using either the relation key or the relation property : + +```haxe +var user = User.manager.get(1); +var groups = UserGroup.manager.search($uid == user.id); +// same as : +var groups = UserGroup.manager.search($user == user); +``` + +The second case is more strictly typed since it does not only check that the key have the same type, and it also safer because it will use null id if the user value is null at runtime. + +## Dynamic Search +If you want to build at runtime you own exact-values search criteria, you can use manager.dynamicSearch that will build the SQL query based on the values you pass it : + +```haxe +var o = { name : "John", phoneNumber : "+818123456" }; +var users = User.manager.dynamicSearch(o); +``` +Please note that you can get runtime errors if your object contain fields that are not in the database table. + +## Serialized Data + +In order to store arbitrary serialized data in a `record-macros` object, you can use the SData type. For example : + +``` +import sys.db.Types +enum PhoneKind { + AtHome; + AtWork; + Mobile; +} +class User extends sys.db.Object { + public var id : SId; + ... + public var phones : SData>; +} +``` +When the phones field is accessed for reading (the first time only), it is unserialized. By default the data is stored as an haxe-serialized string, but you can override the doSerialize and doUnserialize methods of your Manager to have a specific serialization for a specific table or field +When the phones field has been either read or written, a flag will be set to remember that potential changes were made +When the `record-macros` object is either inserted or updated, the modified data is serialized and eventually sent to the database if some actual change have been done +As a consequence, pushing data into the phones Array or directly modifying the phone number will be noticed by the `record-macros` engine. + +The SQL data type for SData is a binary blob, in order to allow any kind of serialization (text or binary), so the actual runtime value of the phones field is a Bytes. It will however only be accessible by reflection, since `record-macros` is changing the phones field into a property. + +## Accessing the record-macros Infos +You can get the database schema by calling the `.dbInfos()` method on the Manager. It will return a `sys.db.RecordInfos` structure. + +## Automatic Insert/Search/Edit Generation +The [dbadmin](https://github.com/ncannasse/dbadmin) project provides an HTML based interface that allows inserting/searching/editing and deleting `record-macros` objects based on the compiled `record-macros` information. It also allows database synchronization based on the `record-macros` schema by automatically detecting differences between the compile time schema and the current DB one. + +## Compatibility +When using MySQL 5.7+, consider disabling [strict mode](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict). Record-macros do not provide sufficient checks (strings length,field default values...) to avoid errors in strict mode. + + + diff --git a/haxelib.json b/haxelib.json new file mode 100644 index 0000000..9b32643 --- /dev/null +++ b/haxelib.json @@ -0,0 +1,11 @@ +{ + "name": "record-macros", + "url": "https://github.com/HaxeFoundation/record-macros", + "license": "MIT", + "classPath": "src", + "tags":["db","spod","orm","sql"], + "description": "Macro-based ORM (object-relational mapping)", + "version": "1.0.0-alpha", + "releasenote": "Initial release", + "contributors":["andyli","ncannasse","simn","waneck"] +} diff --git a/src/sys/db/Manager.hx b/src/sys/db/Manager.hx new file mode 100644 index 0000000..0d31929 --- /dev/null +++ b/src/sys/db/Manager.hx @@ -0,0 +1,723 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package sys.db; +import Reflect; +import sys.db.Connection; +import sys.db.RecordInfos; + +/** + Record Manager : the persistent object database manager. See the tutorial on + Haxe website to learn how to use Record. +**/ +#if !macro @:build(sys.db.RecordMacros.addRtti()) #end +class Manager { + + /* ----------------------------- STATICS ------------------------------ */ + public static var cnx(default, set) : Connection; + public static var lockMode : String; + + private static inline var cache_field = "__cache__"; + + private static var object_cache : haxe.ds.StringMap = new haxe.ds.StringMap(); + private static var init_list : List> = new List(); + + private static var KEYWORDS = { + var h = new haxe.ds.StringMap(); + for( k in "ADD|ALL|ALTER|ANALYZE|AND|AS|ASC|ASENSITIVE|BEFORE|BETWEEN|BIGINT|BINARY|BLOB|BOTH|BY|CALL|CASCADE|CASE|CHANGE|CHAR|CHARACTER|CHECK|COLLATE|COLUMN|CONDITION|CONSTRAINT|CONTINUE|CONVERT|CREATE|CROSS|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|CURRENT_USER|CURSOR|DATABASE|DATABASES|DAY_HOUR|DAY_MICROSECOND|DAY_MINUTE|DAY_SECOND|DEC|DECIMAL|DECLARE|DEFAULT|DELAYED|DELETE|DESC|DESCRIBE|DETERMINISTIC|DISTINCT|DISTINCTROW|DIV|DOUBLE|DROP|DUAL|EACH|ELSE|ELSEIF|ENCLOSED|ESCAPED|EXISTS|EXIT|EXPLAIN|FALSE|FETCH|FLOAT|FLOAT4|FLOAT8|FOR|FORCE|FOREIGN|FROM|FULLTEXT|GRANT|GROUP|HAVING|HIGH_PRIORITY|HOUR_MICROSECOND|HOUR_MINUTE|HOUR_SECOND|IF|IGNORE|IN|INDEX|INFILE|INNER|INOUT|INSENSITIVE|INSERT|INT|INT1|INT2|INT3|INT4|INT8|INTEGER|INTERVAL|INTO|IS|ITERATE|JOIN|KEY|KEYS|KILL|LEADING|LEAVE|LEFT|LIKE|LIMIT|LINES|LOAD|LOCALTIME|LOCALTIMESTAMP|LOCK|LONG|LONGBLOB|LONGTEXT|LOOP|LOW_PRIORITY|MATCH|MEDIUMBLOB|MEDIUMINT|MEDIUMTEXT|MIDDLEINT|MINUTE_MICROSECOND|MINUTE_SECOND|MOD|MODIFIES|NATURAL|NOT|NO_WRITE_TO_BINLOG|NULL|NUMERIC|ON|OPTIMIZE|OPTION|OPTIONALLY|OR|ORDER|OUT|OUTER|OUTFILE|PRECISION|PRIMARY|PROCEDURE|PURGE|READ|READS|REAL|REFERENCES|REGEXP|RELEASE|RENAME|REPEAT|REPLACE|REQUIRE|RESTRICT|RETURN|REVOKE|RIGHT|RLIKE|SCHEMA|SCHEMAS|SECOND_MICROSECOND|SELECT|SENSITIVE|SEPARATOR|SET|SHOW|SMALLINT|SONAME|SPATIAL|SPECIFIC|SQL|SQLEXCEPTION|SQLSTATE|SQLWARNING|SQL_BIG_RESULT|SQL_CALC_FOUND_ROWS|SQL_SMALL_RESULT|SSL|STARTING|STRAIGHT_JOIN|TABLE|TERMINATED|THEN|TINYBLOB|TINYINT|TINYTEXT|TO|TRAILING|TRIGGER|TRUE|UNDO|UNION|UNIQUE|UNLOCK|UNSIGNED|UPDATE|USAGE|USE|USING|UTC_DATE|UTC_TIME|UTC_TIMESTAMP|VALUES|VARBINARY|VARCHAR|VARCHARACTER|VARYING|WHEN|WHERE|WHILE|WITH|WRITE|XOR|YEAR_MONTH|ZEROFILL|ASENSITIVE|CALL|CONDITION|CONNECTION|CONTINUE|CURSOR|DECLARE|DETERMINISTIC|EACH|ELSEIF|EXIT|FETCH|GOTO|INOUT|INSENSITIVE|ITERATE|LABEL|LEAVE|LOOP|MODIFIES|OUT|READS|RELEASE|REPEAT|RETURN|SCHEMA|SCHEMAS|SENSITIVE|SPECIFIC|SQL|SQLEXCEPTION|SQLSTATE|SQLWARNING|TRIGGER|UNDO|UPGRADE|WHILE".split("|") ) + h.set(k.toLowerCase(),true); + h; + } + + private static function set_cnx( c : Connection ) { + cnx = c; + lockMode = (c != null && c.dbName() == "MySQL") ? " FOR UPDATE" : ""; + return c; + } + + /* ---------------------------- BASIC API ----------------------------- */ + + var table_infos : RecordInfos; + var table_name : String; + var table_keys : Array; + var class_proto : { prototype : Dynamic }; + + public function new( classval : Class ) { + var m : Array = haxe.rtti.Meta.getType(classval).rtti; + if( m == null ) throw "Missing @rtti for class " + Type.getClassName(classval); + table_infos = haxe.Unserializer.run(m[0]); + table_name = quoteField(table_infos.name); + table_keys = table_infos.key; + // set the manager and ready for further init + class_proto = cast classval; + #if neko + class_proto.prototype._manager = this; + init_list.add(this); + #end + } + + public function all( ?lock: Bool ) : List { + return unsafeObjects("SELECT * FROM " + table_name,lock); + } + + public macro function get(ethis,id,?lock:haxe.macro.Expr.ExprOf) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { + return RecordMacros.macroGet(ethis,id,lock); + } + + public macro function select(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprOf) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { + return RecordMacros.macroSearch(ethis, cond, options, lock, true); + } + + public macro function search(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprOf) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf> #end { + return RecordMacros.macroSearch(ethis, cond, options, lock); + } + + public macro function count(ethis, cond) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { + return RecordMacros.macroCount(ethis, cond); + } + + public macro function delete(ethis, cond, ?options) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { + return RecordMacros.macroDelete(ethis, cond, options); + } + + public function dynamicSearch( x : {}, ?lock : Bool ) : List { + var s = new StringBuf(); + s.add("SELECT * FROM "); + s.add(table_name); + s.add(" WHERE "); + addCondition(s,x); + return unsafeObjects(s.toString(),lock); + } + + function quote( s : String ) : String { + return getCnx().quote( s ); + } + + /* -------------------------- RECORDOBJECT API -------------------------- */ + + function doUpdateCache( x : T, name : String, v : Dynamic ) { + var cache : { v : Dynamic } = Reflect.field(x, "cache_" + name); + // if the cache has not been fetched (for instance if the field was set by reflection) + // then we directly use the new value + if( cache == null ) + return v; + var v = doSerialize(name, cache.v); + // don't set it since the value might change again later + // Reflect.setField(x, name, v); + return v; + } + + static function getFieldName(field:RecordField):String + { + return switch (field.t) { + case DData | DEnum(_): + "data_" + field.name; + case _: + field.name; + } + } + + function doInsert( x : T ) { + unmake(x); + var s = new StringBuf(); + var fields = new List(); + var values = new List(); + var cache = Reflect.field(x,cache_field); + if (cache == null) + { + Reflect.setField(x,cache_field,cache = {}); + } + + for( f in table_infos.fields ) { + var name = f.name, + fieldName = getFieldName(f); + var v:Dynamic = Reflect.field(x,fieldName); + if( v != null ) { + fields.add(quoteField(name)); + switch( f.t ) { + case DData: v = doUpdateCache(x, name, v); + default: + } + values.add(v); + } else if( !f.isNull ) { + // if the field is not defined, give it a default value on insert + switch( f.t ) { + case DUInt, DTinyInt, DInt, DSingle, DFloat, DFlags(_), DBigInt, DTinyUInt, DSmallInt, DSmallUInt, DMediumInt, DMediumUInt, DEnum(_): + Reflect.setField(x, fieldName, 0); + case DBool: + Reflect.setField(x, fieldName, false); + case DTinyText, DText, DString(_), DSmallText, DSerialized: + Reflect.setField(x, fieldName, ""); + case DSmallBinary, DNekoSerialized, DLongBinary, DBytes(_), DBinary: + Reflect.setField(x, fieldName, haxe.io.Bytes.alloc(0)); + case DDate, DDateTime, DTimeStamp: + // default date might depend on database + case DId, DUId, DBigId, DNull, DInterval, DEncoded, DData: + // no default value for these + } + } + + Reflect.setField(cache, name, v); + } + s.add("INSERT INTO "); + s.add(table_name); + if (fields.length > 0 || cnx.dbName() != "SQLite") + { + s.add(" ("); + s.add(fields.join(",")); + s.add(") VALUES ("); + var first = true; + for( v in values ) { + if( first ) + first = false; + else + s.add(", "); + getCnx().addValue(s,v); + } + s.add(")"); + } else { + s.add(" DEFAULT VALUES"); + } + unsafeExecute(s.toString()); + untyped x._lock = true; + // table with one key not defined : suppose autoincrement + if( table_keys.length == 1 && Reflect.field(x,table_keys[0]) == null ) + Reflect.setField(x,table_keys[0],getCnx().lastInsertId()); + addToCache(x); + } + + inline function isBinary( t : RecordInfos.RecordType ) { + return switch( t ) { + case DSmallBinary, DNekoSerialized, DLongBinary, DBytes(_), DBinary: true; + //case DData: true // -- disabled for implementation purposes + default: false; + }; + } + + inline function hasBinaryChanged( a : haxe.io.Bytes, b : haxe.io.Bytes ) { + return a != b && (a == null || b == null || a.compare(b) != 0); + } + + function doUpdate( x : T ) { + if( untyped !x._lock ) + throw "Cannot update a not locked object"; + var upd = getUpdateStatement(x); + if (upd == null) return; + unsafeExecute(upd); + } + + function getUpdateStatement( x : T ):Null + { + unmake(x); + var s = new StringBuf(); + s.add("UPDATE "); + s.add(table_name); + s.add(" SET "); + var cache = Reflect.field(x,cache_field); + var mod = false; + for( f in table_infos.fields ) { + if (table_keys.indexOf(f.name) >= 0) + continue; + var name = f.name, + fieldName = getFieldName(f); + var v : Dynamic = Reflect.field(x,fieldName); + var vc : Dynamic = Reflect.field(cache,name); + if( cache == null || v != vc ) { + switch( f.t ) { + case DSmallBinary, DNekoSerialized, DLongBinary, DBytes(_), DBinary: + if ( !hasBinaryChanged(v,vc) ) + continue; + case DData: + v = doUpdateCache(x, name, v); + if( !hasBinaryChanged(v,vc) ) + continue; + default: + } + if( mod ) + s.add(", "); + else + mod = true; + s.add(quoteField(name)); + s.add(" = "); + getCnx().addValue(s,v); + if ( cache != null ) + Reflect.setField(cache,name,v); + } + } + if( !mod ) + return null; + s.add(" WHERE "); + addKeys(s,x); + return s.toString(); + } + + function doDelete( x : T ) { + var s = new StringBuf(); + s.add("DELETE FROM "); + s.add(table_name); + s.add(" WHERE "); + addKeys(s,x); + unsafeExecute(s.toString()); + removeFromCache(x); + } + + function doLock( i : T ) { + if( untyped i._lock ) + return; + var s = new StringBuf(); + s.add("SELECT * FROM "); + s.add(table_name); + s.add(" WHERE "); + addKeys(s, i); + // will force sync + if( unsafeObject(s.toString(),true) != i ) + throw "Could not lock object (was deleted ?); try restarting transaction"; + } + + function objectToString( it : T ) : String { + var s = new StringBuf(); + s.add(table_name); + if( table_keys.length == 1 ) { + s.add("#"); + s.add(Reflect.field(it,table_keys[0])); + } else { + s.add("("); + var first = true; + for( f in table_keys ) { + if( first ) + first = false; + else + s.add(","); + s.add(quoteField(f)); + s.add(":"); + s.add(Reflect.field(it,f)); + } + s.add(")"); + } + return s.toString(); + } + + function doSerialize( field : String, v : Dynamic ) : haxe.io.Bytes { + var s = new haxe.Serializer(); + s.useEnumIndex = true; + s.serialize(v); + var str = s.toString(); + #if neko + return neko.Lib.bytesReference(str); + #else + return haxe.io.Bytes.ofString(str); + #end + } + + function doUnserialize( field : String, b : haxe.io.Bytes ) : Dynamic { + if( b == null ) + return null; + var str; + #if neko + str = neko.Lib.stringReference(b); + #else + str = b.toString(); + #end + if( str == "" ) + return null; + return haxe.Unserializer.run(str); + } + + /* ---------------------------- INTERNAL API -------------------------- */ + + function normalizeCache(x:CacheType) + { + for (f in Reflect.fields(x) ) + { + var val:Dynamic = Reflect.field(x,f), info = table_infos.hfields.get(f); + if (info != null) + { + if (val != null) switch (info.t) { + case DDate, DDateTime if (!Std.is(val,Date)): + if (Std.is(val,Float)) + { + val = Date.fromTime(val); + } else { + var v = val + ""; + var index = v.indexOf('.'); + if (index >= 0) + v = v.substr(0,index); + val = Date.fromString(v); + } + case DSmallBinary, DLongBinary, DBinary, DBytes(_), DData if (Std.is(val, String)): + val = haxe.io.Bytes.ofString(val); + case DString(_) | DTinyText | DSmallText | DText if(!Std.is(val,String)): + val = val + ""; +#if (cs && erase_generics) + // on C#, SQLite Ints are returned as Int64 + case DInt if (!Std.is(val,Int)): + val = cast(val,Int); +#end + case DBool if (!Std.is(val,Bool)): + if (Std.is(val,Int)) + val = val != 0; + else if (Std.is(val, String)) switch (val.toLowerCase()) { + case "1", "true": val = true; + case "0", "false": val = false; + } + case DFloat if (Std.is(val,String)): + val = Std.parseFloat(val); + case _: + } + Reflect.setField(x, f, val); + } + } + } + + function cacheObject( x : T, lock : Bool ) { + #if neko + var o = untyped __dollar__new(x); + untyped __dollar__objsetproto(o, class_proto.prototype); + #else + var o : T = Type.createEmptyInstance(cast class_proto); + untyped o._manager = this; + #end + normalizeCache(x); + for (f in Reflect.fields(x) ) + { + var val:Dynamic = Reflect.field(x,f), info = table_infos.hfields.get(f); + if (info != null) + { + var fieldName = getFieldName(info); + Reflect.setField(o, fieldName, val); + } + } + Reflect.setField(o,cache_field,x); + addToCache(o); + untyped o._lock = lock; + return o; + } + + function make( x : T ) { + } + + function unmake( x : T ) { + } + + function quoteField(f : String) { + return KEYWORDS.exists(f.toLowerCase()) ? "`"+f+"`" : f; + } + + function addKeys( s : StringBuf, x : {} ) { + var first = true; + for( k in table_keys ) { + if( first ) + first = false; + else + s.add(" AND "); + s.add(quoteField(k)); + s.add(" = "); + var f = Reflect.field(x,k); + if( f == null ) + throw ("Missing key "+k); + getCnx().addValue(s,f); + } + } + + function unsafeExecute( sql : String ) { + return getCnx().request(sql); + } + + public function unsafeObject( sql : String, lock : Bool ) : T { + if( lock != false ) { + lock = true; + sql += getLockMode(); + } + var r = unsafeExecute(sql); + var r = r.hasNext() ? r.next() : null; + if( r == null ) + return null; + normalizeCache(r); + var c = getFromCache(r,lock); + if( c != null ) + return c; + r = cacheObject(r,lock); + make(r); + return r; + } + + public function unsafeObjects( sql : String, lock : Bool ) : List { + if( lock != false ) { + lock = true; + sql += getLockMode(); + } + var l = unsafeExecute(sql).results(); + var l2 = new List(); + for( x in l ) { + normalizeCache(x); + var c = getFromCache(x,lock); + if( c != null ) + l2.add(c); + else { + x = cacheObject(x,lock); + make(x); + l2.add(x); + } + } + return l2; + } + + public function unsafeCount( sql : String ) { + return unsafeExecute(sql).getIntResult(0); + } + + public function unsafeDelete( sql : String ) { + unsafeExecute(sql); + } + + public function unsafeGet( id : Dynamic, ?lock : Bool ) : T { + if( lock == null ) lock = true; + if( table_keys.length != 1 ) + throw "Invalid number of keys"; + if( id == null ) + return null; + var x : Dynamic = getFromCacheKey(Std.string(id) + table_name); + if( x != null && (!lock || x._lock) ) + return x; + var s = new StringBuf(); + s.add("SELECT * FROM "); + s.add(table_name); + s.add(" WHERE "); + s.add(quoteField(table_keys[0])); + s.add(" = "); + getCnx().addValue(s,id); + return unsafeObject(s.toString(), lock); + } + + public function unsafeGetWithKeys( keys : { }, ?lock : Bool ) : T { + if( lock == null ) lock = true; + var x : Dynamic = getFromCacheKey(makeCacheKey(cast keys)); + if( x != null && (!lock || x._lock) ) + return x; + var s = new StringBuf(); + s.add("SELECT * FROM "); + s.add(table_name); + s.add(" WHERE "); + addKeys(s,keys); + return unsafeObject(s.toString(),lock); + } + + public function unsafeGetId( o : T ) : Dynamic { + return o == null ? null : Reflect.field(o, table_keys[0]); + } + + public static function nullCompare( a : String, b : String, eq : Bool ) { + if (a == null || a == 'NULL') { + return eq ? '$b IS NULL' : '$b IS NOT NULL'; + } else if (b == null || b == 'NULL') { + return eq ? '$a IS NULL' : '$a IS NOT NULL'; + } + // we can't use a null-safe operator here + if( cnx.dbName() != "MySQL" ) + return a + (eq ? " = " : " != ") + b; + var sql = a+" <=> "+b; + if( !eq ) sql = "NOT("+sql+")"; + return sql; + } + + function addCondition(s : StringBuf,x) { + var first = true; + if( x != null ) + for( f in Reflect.fields(x) ) { + if( first ) + first = false; + else + s.add(" AND "); + s.add(quoteField(f)); + var d = Reflect.field(x,f); + if( d == null ) + s.add(" IS NULL"); + else { + s.add(" = "); + getCnx().addValue(s,d); + } + } + if( first ) + s.add("TRUE"); + } + + /* --------------------------- MISC API ------------------------------ */ + + public function dbClass() : Class { + return cast class_proto; + } + + public function dbInfos() { + return table_infos; + } + + function getCnx() { + return cnx; + } + + function getLockMode() { + return lockMode; + } + + /** + Remove the cached value for the given Object field : this will ensure + that the value is updated when calling .update(). This is necessary if + you are modifying binary data in-place since the cache will be modified + as well. + **/ + public function forceUpdate( o : T, field : String ) { + // set a reference that will ensure != and .compare() != 0 + Reflect.setField(Reflect.field(o,cache_field),field,null); + } + + /* --------------------------- INIT / CLEANUP ------------------------- */ + + public static function initialize() { + var l = init_list; + init_list = new List(); + for( m in l ) + for( r in m.table_infos.relations ) + m.initRelation(r); + } + + public static function cleanup() { + object_cache = new haxe.ds.StringMap(); + } + + function initRelation( r : RecordInfos.RecordRelation ) { + // setup getter/setter + var spod : Dynamic = Type.resolveClass(r.type); + if( spod == null ) throw "Missing spod type " + r.type; + var manager : Manager = spod.manager; + var hprop = "__"+r.prop; + var hkey = r.key; + var lock = r.lock; + if( manager == null || manager.table_keys == null ) throw ("Invalid manager for relation "+table_name+":"+r.prop); + if( manager.table_keys.length != 1 ) throw ("Relation " + r.prop + "(" + r.key + ") on a multiple key table"); + } + + function __get( x : Dynamic, prop : String, key : String, lock ) { + var v = Reflect.field(x,prop); + if( v != null ) + return v; + var y = unsafeGet(Reflect.field(x, key), lock); + Reflect.setField(x,prop,v); + return y; + } + + function __set( x : Dynamic, prop : String, key : String, v : T ) { + Reflect.setField(x,prop,v); + if( v == null ) + Reflect.setField(x,key,null); + else + Reflect.setField(x,key,Reflect.field(v,table_keys[0])); + return v; + } + + /* ---------------------------- OBJECT CACHE -------------------------- */ + + function makeCacheKey( x : T ) : String { + if( table_keys.length == 1 ) { + var k = Reflect.field(x,table_keys[0]); + if( k == null ) + throw("Missing key "+table_keys[0]); + return Std.string(k)+table_name; + } + var s = new StringBuf(); + for( k in table_keys ) { + var v = Reflect.field(x,k); + if( k == null ) + throw("Missing key "+k); + s.add(v); + s.add("#"); + } + s.add(table_name); + return s.toString(); + } + + function addToCache( x : CacheType ) { + object_cache.set(makeCacheKey(x),x); + } + + function removeFromCache( x : CacheType ) { + object_cache.remove(makeCacheKey(x)); + } + + function getFromCacheKey( key : String ) : T { + return cast object_cache.get(key); + } + + function getFromCache( x : CacheType, lock : Bool ) : T { + var c : Dynamic = object_cache.get(makeCacheKey(x)); + if( c != null && lock && !c._lock ) { + // synchronize the fields since our result is up-to-date ! + for( f in Reflect.fields(c) ) + Reflect.deleteField(c,f); + for (f in table_infos.fields) + { + var name = f.name, + fieldName = getFieldName(f); + Reflect.setField(c,fieldName,Reflect.field(x,name)); + } + // mark as locked + c._lock = true; + // restore our manager + #if !neko + untyped c._manager = this; + #end + // use the new object as our cache of fields + Reflect.setField(c,cache_field,x); + // remake object + make(c); + } + return c; + } + + /* ---------------------------- QUOTES -------------------------- */ + + public static function quoteAny( v : Dynamic ) { + if (v == null) { + return 'NULL'; + } + + var s = new StringBuf(); + cnx.addValue(s, v); + return s.toString(); + } + + public static function quoteList( v : String, it : Iterable ) { + var b = new StringBuf(); + var first = true; + if( it != null ) + for( v in it ) { + if( first ) first = false else b.addChar(','.code); + cnx.addValue(b, v); + } + if( first ) + return "FALSE"; + return v + " IN (" + b.toString() + ")"; + } + + // We need Bytes.toString to not be DCE'd. See #1937 + @:keep static function __depends() { return haxe.io.Bytes.alloc(0).toString(); } +} + +private typedef CacheType = Dynamic; diff --git a/src/sys/db/Object.hx b/src/sys/db/Object.hx new file mode 100644 index 0000000..bea3435 --- /dev/null +++ b/src/sys/db/Object.hx @@ -0,0 +1,76 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +/** + Record Object : the persistent object base type. See the tutorial on Haxe + website to learn how to use Record. +**/ +@:keepSub +@:autoBuild(sys.db.RecordMacros.macroBuild()) @:skipFields +class Object { + + var _lock(default,never) : Bool; + var _manager(default,never) : sys.db.Manager; +#if !neko + @:keep var __cache__:Dynamic; +#end + + public function new() { + #if !neko + if( _manager == null ) untyped _manager = __getManager(); + #end + } + +#if !neko + private function __getManager():sys.db.Manager + { + var cls:Dynamic = Type.getClass(this); + return cls.manager; + } +#end + + public function insert() { + untyped _manager.doInsert(this); + } + + public function update() { + untyped _manager.doUpdate(this); + } + + public function lock() { + untyped _manager.doLock(this); + } + + public function delete() { + untyped _manager.doDelete(this); + } + + public function isLocked() { + return _lock; + } + + public function toString() : String { + return untyped _manager.objectToString(this); + } + +} diff --git a/src/sys/db/RecordInfos.hx b/src/sys/db/RecordInfos.hx new file mode 100644 index 0000000..e53732e --- /dev/null +++ b/src/sys/db/RecordInfos.hx @@ -0,0 +1,85 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +enum RecordType { + DId; + DInt; + DUId; + DUInt; + DBigId; + DBigInt; + DSingle; + DFloat; + DBool; + DString( n : Int ); + DDate; + DDateTime; + DTimeStamp; + DTinyText; + DSmallText; + DText; + DSmallBinary; + DLongBinary; + DBinary; + DBytes( n : Int ); + DEncoded; + DSerialized; + DNekoSerialized; + DFlags( flags : Array, autoSize : Bool ); + DTinyInt; + DTinyUInt; + DSmallInt; + DSmallUInt; + DMediumInt; + DMediumUInt; + DData; + DEnum( name : String ); + // specific for intermediate calculus + DInterval; + DNull; +} + +typedef RecordField = { + var name : String; + var t : RecordType; + var isNull : Bool; +} + +typedef RecordRelation = { + var prop : String; + var key : String; + var type : String; + var module : String; + var cascade : Bool; + var lock : Bool; + var isNull : Bool; +} + +typedef RecordInfos = { + var name : String; + var key : Array; + var fields : Array; + var hfields : Map; + var relations : Array; + var indexes : Array<{ keys : Array, unique : Bool }>; +} diff --git a/src/sys/db/RecordMacros.hx b/src/sys/db/RecordMacros.hx new file mode 100644 index 0000000..4b51c3a --- /dev/null +++ b/src/sys/db/RecordMacros.hx @@ -0,0 +1,1441 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package sys.db; +import sys.db.RecordInfos; +import haxe.macro.Expr; +import haxe.macro.Type.VarAccess; +#if macro +import haxe.macro.Context; +using haxe.macro.TypeTools; +#end +using Lambda; + +private typedef SqlFunction = { + var name : String; + var params : Array; + var ret : RecordType; + var sql : String; +} + +class RecordMacros { + + static var GLOBAL = null; + static var simpleString = ~/^[A-Za-z0-9 ]*$/; + + var isNull : Bool; + var manager : Expr; + var inf : RecordInfos; + var g : { + var cache : haxe.ds.StringMap; + var types : haxe.ds.StringMap; + var functions : haxe.ds.StringMap; + }; + + function new(c) { + if( GLOBAL != null ) + g = GLOBAL; + else { + g = initGlobals(); + GLOBAL = g; + } + inf = getRecordInfos(c); + } + + function initGlobals() + { + var cache = new haxe.ds.StringMap(); + var types = new haxe.ds.StringMap(); + for( c in Type.getEnumConstructs(RecordType) ) { + var e : Dynamic = Reflect.field(RecordType, c); + if( Std.is(e, RecordType) ) + types.set("S"+c.substr(1), e); + } + types.remove("SNull"); + var functions = new haxe.ds.StringMap(); + for( f in [ + { name : "now", params : [], ret : DDateTime, sql : "NOW($)" }, + { name : "curDate", params : [], ret : DDate, sql : "CURDATE($)" }, + { name : "seconds", params : [DFloat], ret : DInterval, sql : "INTERVAL $ SECOND" }, + { name : "minutes", params : [DFloat], ret : DInterval, sql : "INTERVAL $ MINUTE" }, + { name : "hours", params : [DFloat], ret : DInterval, sql : "INTERVAL $ HOUR" }, + { name : "days", params : [DFloat], ret : DInterval, sql : "INTERVAL $ DAY" }, + { name : "months", params : [DFloat], ret : DInterval, sql : "INTERVAL $ MONTH" }, + { name : "years", params : [DFloat], ret : DInterval, sql : "INTERVAL $ YEAR" }, + { name : "date", params : [DDateTime], ret : DDate, sql : "DATE($)" }, + ]) + functions.set(f.name, f); + return { cache : cache, types : types, functions : functions }; + } + + public dynamic function error( msg : String, pos : Position ) : Dynamic { + #if macro + Context.error(msg, pos); + #else + throw msg; + #end + return null; + } + + public dynamic function typeof( e : Expr ) : haxe.macro.Type { + #if macro + return Context.typeof(e); + #else + throw "not implemented"; + return null; + #end + } + + public dynamic function follow( t : haxe.macro.Type, ?once ) : haxe.macro.Type { + #if macro + return Context.follow(t,once); + #else + throw "not implemented"; + return null; + #end + } + + public dynamic function getManager( t : haxe.macro.Type, p : Position ) : RecordMacros { + #if macro + return getManagerInfos(t, p); + #else + throw "not implemented"; + return null; + #end + } + + public dynamic function resolveType( name : String, ?module : String ) : haxe.macro.Type { + #if macro + if (module != null) + { + var m = Context.getModule(module); + for (t in m) + { + if (t.toString() == name) + return t; + } + } + + return Context.getType(name); + #else + throw "not implemented"; + return null; + #end + } + + function makeInt( t : haxe.macro.Type ) { + switch( t ) { + case TInst(c, _): + var name = c.toString(); + if( name.charAt(0) == "I" ) + return Std.parseInt(name.substr(1)); + default: + } + throw "Unsupported " + Std.string(t); + } + + function makeRecord( t : haxe.macro.Type ) { + switch( t ) { + case TInst(c, _): + var name = c.toString(); + var cl = c.get(); + var csup = cl.superClass; + while( csup != null ) { + if( csup.t.toString() == "sys.db.Object" ) + return c; + csup = csup.t.get().superClass; + } + case TType(t, p) | TAbstract(t, p): + var name = t.toString(); + if( p.length == 1 && (name == "Null" || name == "sys.db.SNull") ) { + isNull = true; + return makeRecord(p[0]); + } + default: + } + return null; + } + + function getFlags( t : haxe.macro.Type ) { + switch( t ) { + case TEnum(e,_): + var cl = e.get().names; + if( cl.length > 1 ) { + var prefix = cl[0]; + for( c in cl ) + while( prefix.length > 0 && c.substr(0, prefix.length) != prefix ) + prefix = prefix.substr(0, -1); + for( i in 0...cl.length ) + cl[i] = cl[i].substr(prefix.length); + } + if( cl.length > 31 ) throw "Too many flags"; + return cl; + default: + throw "Flags parameter should be an enum"; + } + } + + function makeType( t : haxe.macro.Type ) { + switch( t ) { + case TInst(c, _): + var name = c.toString(); + return switch( name ) { + case "Int": DInt; + case "Float": DFloat; + case "String": DText; + case "Date": DDateTime; + case "haxe.io.Bytes": DBinary; + default: throw "Unsupported Record Type " + name; + } + case TAbstract(a, p): + var name = a.toString(); + return switch( name ) { + case "Int": DInt; + case "Float": DFloat; + case "Bool": DBool; + case "Null": isNull = true; return makeType(p[0]); + case _ if (!a.get().meta.has(':coreType')): + var a = a.get(); +#if macro + makeType(a.type.applyTypeParameters(a.params, p)); +#else + makeType(a.type); +#end + default: throw "Unsupported Record Type " + name; + } + case TEnum(e, _): + var name = e.toString(); + return switch( name ) { + case "Bool": DBool; + default: + throw "Unsupported Record Type " + name + " (enums must be wrapped with SData or SEnum)"; + } + case TType(td, p): + var name = td.toString(); + if( StringTools.startsWith(name, "sys.db.") ) + name = name.substr(7); + var k = g.types.get(name); + if( k != null ) return k; + if( p.length == 1 ) + switch( name ) { + case "SString": return DString(makeInt(p[0])); + case "SBytes": return DBytes(makeInt(p[0])); + case "SNull", "Null": isNull = true; return makeType(p[0]); + case "SFlags": return DFlags(getFlags(p[0]),false); + case "SSmallFlags": return DFlags(getFlags(p[0]),true); + case "SData": return DData; + case "SEnum": + switch( p[0] ) { + case TEnum(en, _): + var e = en.get(); + var count = 0, hasParam = false; + for( c in e.constructs ) { + count++; + switch( c.type ) { + case TFun(_): + hasParam = true; + default: + } + } + if( hasParam ) + throw "You can't use SEnum if the enum have parameters, try SData instead"; + if( count >= 256 ) + throw "Too many enum constructors"; + return DEnum(en.toString()); + default: + // should cause another error + } + default: + } + return makeType(follow(t, true)); + case TLazy(f): + return makeType(f()); + default: + } + throw "Unsupported Record Type " + Std.string(t); + } + + function makeIdent( e : Expr ) { + return switch( e.expr ) { + case EConst(c): + switch( c ) { + case CIdent(s): s; + default: error("Identifier expected", e.pos); + } + default: error("Identifier expected", e.pos); + } + } + + function getRecordInfos( c : haxe.macro.Type.Ref ) : RecordInfos { + var cname = c.toString(); + var i = g.cache.get(cname); + if( i != null ) return i; + i = { + key : null, + name : cname.split(".").pop(), // remove package name + fields : [], + hfields : new haxe.ds.StringMap(), + relations : [], + indexes : [], + }; + g.cache.set(cname, i); + var c = c.get(); + var fieldsPos = new haxe.ds.StringMap(); + var fields = c.fields.get(); + var csup = c.superClass; + while( csup != null ) { + var c = csup.t.get(); + if( !c.meta.has(":skipFields") ) + fields = c.fields.get().concat(fields); + csup = c.superClass; + } + for( f in fields ) { + fieldsPos.set(f.name, f.pos); + switch( f.kind ) { + case FMethod(_): + // skip methods + continue; + case FVar(g, s): + // skip not-db fields + if( f.meta.has(":skip") ) + continue; + // handle relations + if( f.meta.has(":relation") ) { + if( !Type.enumEq(g,AccCall) || !Type.enumEq(s,AccCall) ) + error("Relation should be (dynamic,dynamic)", f.pos); + for( m in f.meta.get() ) { + if( m.name != ":relation" ) continue; + if( m.params.length == 0 ) error("Missing relation key", m.pos); + var params = []; + for( p in m.params ) + params.push({ i : makeIdent(p), p : p.pos }); + isNull = false; + var t = makeRecord(f.type); + if( t == null ) error("Relation type should be a sys.db.Object", f.pos); + var mod = t.get().module; + var r = { + prop : f.name, + key : params.shift().i, + type : t.toString(), + module : mod, + cascade : false, + lock : false, + isNull : isNull, + }; + // setup flags + for( p in params ) + switch( p.i ) { + case "lock": r.lock = true; + case "cascade": r.cascade = true; + default: error("Unknown relation flag", p.p); + } + i.relations.push(r); + } + continue; + } + switch( g ) { + case AccCall: + if( !f.meta.has(":data") ) + error("Relation should be defined with @:relation(key)", f.pos); + default: + } + } + isNull = false; + var fi = { + name : f.name, + t : try makeType(f.type) catch( e : String ) error(e,f.pos), + isNull : isNull, + }; + var isId = switch( fi.t ) { + case DId, DUId, DBigId: true; + default: i.key == null && fi.name == "id"; + } + if( isId ) { + switch(fi.t) + { + case DInt: + fi.t = DId; + case DUInt: + fi.t = DUId; + case DBigInt: + fi.t = DBigId; + case _: + } + if( i.key == null ) i.key = [fi.name] else error("Multiple table id declaration", f.pos); + } + i.fields.push(fi); + i.hfields.set(fi.name, fi); + } + // create fields for undeclared relations keys : + for( r in i.relations ) { + var field = fields.find(function(f) return f.name == r.prop); + var f = i.hfields.get(r.key); + var relatedInf = getRecordInfos(makeRecord(resolveType(r.type, r.module))); + if (relatedInf.key.length > 1) + error('The relation ${r.prop} is invalid: Type ${r.type} has multiple keys, which is not supported',field.pos); + var relatedKey = relatedInf.key[0]; + var relatedKeyType = switch(relatedInf.hfields.get(relatedKey).t) + { + case DId: DInt; + case DUId: DUInt; + case DBigId: DBigInt; + case t = DString(_): t; + case t: error('Unexpected id type $t for the relation. Use either SId, SInt, SUId, SUInt, SBigID, SBigInt or SString', field.pos); + } + + if( f == null ) { + f = { + name : r.key, + t : relatedKeyType, + isNull : r.isNull, + }; + i.fields.push(f); + i.hfields.set(f.name, f); + } else { + var pos = fieldsPos.get(f.name); + if( f.t != relatedKeyType) error("Relation source and field should have same type", pos); + if( f.isNull != r.isNull ) error("Relation and field should have same nullability", pos); + } + } + // process class metadata + for( m in c.meta.get() ) + switch( m.name ) { + case ":id": + i.key = []; + for( p in m.params ) { + var id = makeIdent(p); + if( !i.hfields.exists(id) ) + error("This field does not exists", p.pos); + i.key.push(id); + } + if( i.key.length == 0 ) error("Invalid :id", m.pos); + if (i.key.length == 1 ) + { + var field = i.hfields.get(i.key[0]); + switch(field.t) + { + case DInt: + field.t = DId; + case DUInt: + field.t = DUId; + case DBigInt: + field.t = DBigId; + case _: + } + } + case ":index": + var idx = []; + for( p in m.params ) idx.push(makeIdent(p)); + var unique = idx[idx.length - 1] == "unique"; + if( unique ) idx.pop(); + if( idx.length == 0 ) error("Invalid :index", m.pos); + for( k in 0...idx.length ) + if( !i.hfields.exists(idx[k]) ) + error("This field does not exists", m.params[k].pos); + i.indexes.push( { keys : idx, unique : unique } ); + case ":table": + if( m.params.length != 1 ) error("Invalid :table", m.pos); + i.name = switch( m.params[0].expr ) { + case EConst(c): switch( c ) { case CString(s): s; default: null; } + default: null; + }; + if( i.name == null ) error("Invalid :table value", m.params[0].pos); + default: + } + // check primary key defined + if( i.key == null ) + error("Table is missing unique id, use either SId, SUId, SBigID or @:id", c.pos); + return i; + } + + function quoteField( f : String ) { + var m : { private var KEYWORDS : haxe.ds.StringMap; } = Manager; + return m.KEYWORDS.exists(f.toLowerCase()) ? "`"+f+"`" : f; + } + + function initManager( pos : Position ) { + manager = { expr : EField({ expr : EField({ expr : EConst(CIdent("sys")), pos : pos },"db"), pos : pos }, "Manager"), pos : pos }; + } + + inline function makeString( s : String, pos ) { + return { expr : EConst(CString(s)), pos : pos }; + } + + inline function makeOp( op : String, e1, e2, pos ) { + return sqlAdd(sqlAddString(e1,op),e2,pos); + } + + inline function sqlAdd( e1 : Expr, e2 : Expr, pos : Position ) { + return { expr : EBinop(OpAdd, e1, e2), pos : pos }; + } + + inline function sqlAddString( sql : Expr, s : String ) { + return { expr : EBinop(OpAdd, sql, makeString(s,sql.pos)), pos : sql.pos }; + } + + function sqlQuoteValue( v : Expr, t : RecordType, isNull : Bool ) { + switch( v.expr ) { + case EConst(c): + switch( c ) { + case CInt(_), CFloat(_): return v; + case CString(s): + if( simpleString.match(s) ) return { expr : EConst(CString("'"+s+"'")), pos : v.pos }; + case CIdent(n): + switch( n ) { + case "null": return { expr : EConst(CString("NULL")), pos : v.pos }; + case "true": return { expr : EConst(CString("TRUE")), pos : v.pos }; + case "false": return { expr : EConst(CString("FALSE")), pos : v.pos }; + } + default: + } + default: + } + return { expr : ECall( { expr : EField(manager, "quoteAny"), pos : v.pos }, [ensureType(v,t,isNull)]), pos : v.pos } + } + + inline function sqlAddValue( sql : Expr, v : Expr, t : RecordType, isNull : Bool ) { + return { expr : EBinop(OpAdd, sql, sqlQuoteValue(v,t, isNull)), pos : sql.pos }; + } + + function unifyClass( t : RecordType ) { + return switch( t ) { + case DId, DInt, DUId, DUInt, DEncoded, DFlags(_), DTinyInt, DTinyUInt, DSmallInt, DSmallUInt, DMediumInt, DMediumUInt: 0; + case DBigId, DBigInt, DSingle, DFloat: 1; + case DBool: 2; + case DString(_), DTinyText, DSmallText, DText, DSerialized: 3; + case DDate, DDateTime, DTimeStamp: 4; + case DSmallBinary, DLongBinary, DBinary, DBytes(_), DNekoSerialized, DData: 5; + case DInterval: 6; + case DNull: 7; + case DEnum(_): -1; + }; + } + + function tryUnify( t, rt ) { + if( t == rt ) return true; + var c = unifyClass(t); + if( c < 0 ) return Type.enumEq(t, rt); + var rc = unifyClass(rt); + return c == rc || (c == 0 && rc == 1); // allow Int-to-Float expansion + } + + function typeStr( t : RecordType ) { + return Std.string(t).substr(1); + } + + function canStringify( t : RecordType ) { + return switch( unifyClass(t) ) { + case 0, 1, 2, 3, 4, 5, 7: true; + default: false; + }; + } + + function convertType( t : RecordType ) { + var pack = []; + return TPath( { + name : switch( unifyClass(t) ) { + case 0: "Int"; + case 1: "Float"; + case 2: "Bool"; + case 3: "String"; + case 4: "Date"; + case 5: pack = ["haxe", "io"]; "Bytes"; + default: throw "assert"; + }, + pack : pack, + params : [], + sub : null, + }); + } + + function unify( t : RecordType, rt : RecordType, pos : Position ) { + if( !tryUnify(t, rt) ) + error(typeStr(t) + " should be " + typeStr(rt), pos); + } + + function buildCmp( op, e1, e2, pos ) { + var r1 = buildCond(e1); + var r2 = buildCond(e2); + unify(r2.t, r1.t, e2.pos); + if( !tryUnify(r1.t, DFloat) && !tryUnify(r1.t, DDate) && !tryUnify(r1.t, DText) ) + unify(r1.t, DFloat, e1.pos); + return { sql : makeOp(op, r1.sql, r2.sql, pos), t : DBool, n : r1.n || r2.n }; + } + + function buildNum( op, e1, e2, pos ) { + var r1 = buildCond(e1); + var r2 = buildCond(e2); + var c1 = unifyClass(r1.t); + var c2 = unifyClass(r2.t); + if( c1 > 1 ) { + if( op == "-" && tryUnify(r1.t, DDateTime) && tryUnify(r2.t,DInterval) ) + return { sql : makeOp(op, r1.sql, r2.sql, pos), t : DDateTime, n : r1.n }; + unify(r1.t, DInt, e1.pos); + } + if( c2 > 1 ) unify(r2.t, DInt, e2.pos); + return { sql : makeOp(op, r1.sql, r2.sql, pos), t : (c1 + c2) == 0 ? DInt : DFloat, n : r1.n || r2.n }; + } + + function buildInt( op, e1, e2, pos ) { + var r1 = buildCond(e1); + var r2 = buildCond(e2); + unify(r1.t, DInt, e1.pos); + unify(r2.t, DInt, e2.pos); + return { sql : makeOp(op, r1.sql, r2.sql, pos), t : DInt, n : r1.n || r2.n }; + } + + function buildEq( eq, e1 : Expr, e2, pos ) { + var r1 = null; + switch( e1.expr ) { + case EConst(c): + switch( c ) { + case CIdent(i): + if( i.charCodeAt(0) == "$".code ) { + var tmp = { field : i.substr(1), expr : e2 }; + var f = getField(tmp); + r1 = { sql : makeString(quoteField(tmp.field), e1.pos), t : f.t, n : f.isNull }; + e2 = tmp.expr; + switch( f.t ) { + case DEnum(e): + var ok = false; + switch( e2.expr ) { + case EConst(c): + switch( c ) { + case CIdent(n): + if( n.charCodeAt(0) == '$'.code ) + ok = true; + else switch( resolveType(e) ) { + case TEnum(e, _): + var c = e.get().constructs.get(n); + if( c == null ) { + if( n == "null" ) + return { sql : sqlAddString(r1.sql, eq ? " IS NULL" : " IS NOT NULL"), t : DBool, n : false }; + } else { + return { sql : makeOp(eq?" = ":" != ", r1.sql, { expr : EConst(CInt(Std.string(c.index))), pos : e2.pos }, pos), t : DBool, n : r1.n }; + } + default: + } + default: + } + default: + } + if( !ok ) + { + var epath = e.split('.'); + var ename = epath.pop(); + var etype = TPath({ name:ename, pack:epath }); + if (r1.n) { + return { sql: macro $manager.nullCompare(${r1.sql}, { var tmp = @:pos(e2.pos) (${e2} : $etype); tmp == null ? null : (std.Type.enumIndex(tmp) + ''); }, ${eq ? macro true : macro false}), t : DBool, n: true }; + } else { + var expr = macro { @:pos(e2.pos) var tmp : $etype = $e2; (tmp == null ? null : (std.Type.enumIndex(tmp) + '')); }; + return { sql: makeOp(eq?" = ":" != ", r1.sql, expr, pos), t : DBool, n : r1.n }; + } + } + default: + } + } + default: + } + default: + } + if( r1 == null ) + r1 = buildCond(e1); + var r2 = buildCond(e2); + if( r2.t == DNull ) { + if( !r1.n ) + error("Expression can't be null", e1.pos); + return { sql : sqlAddString(r1.sql, eq ? " IS NULL" : " IS NOT NULL"), t : DBool, n : false }; + } else { + unify(r2.t, r1.t, e2.pos); + unify(r1.t, r2.t, e1.pos); + } + var sql; + // use some different operators if there is a possibility for comparing two NULLs + if( r1.n || r2.n ) + sql = { expr : ECall({ expr : EField(manager,"nullCompare"), pos : pos },[r1.sql,r2.sql,{ expr : EConst(CIdent(eq?"true":"false")), pos : pos }]), pos : pos }; + else + sql = makeOp(eq?" = ":" != ", r1.sql, r2.sql, pos); + return { sql : sql, t : DBool, n : r1.n || r2.n }; + } + + function buildDefault( cond : Expr ) { + var t = typeof(cond); + isNull = false; + var d = try makeType(t) catch( e : String ) try makeType(follow(t)) catch( e : String ) error("Unsupported type " + Std.string(t), cond.pos); + return { sql : sqlQuoteValue(cond, d, isNull), t : d, n : isNull }; + } + + function getField( f : { field : String, expr : Expr } ) { + var fi = inf.hfields.get(f.field); + if( fi == null ) { + for( r in inf.relations ) + if( r.prop == f.field ) { + var path = r.type.split("."); + var p = f.expr.pos; + path.push("manager"); + var first = path.shift(); + var mpath = { expr : EConst(CIdent(first)), pos : p }; + for ( e in path ) + mpath = { expr : EField(mpath, e), pos : p }; + var m = getManager(typeof(mpath),p); + var getid = { expr : ECall( { expr : EField(mpath, "unsafeGetId"), pos : p }, [f.expr]), pos : p }; + f.field = r.key; + f.expr = ensureType(getid, m.inf.hfields.get(m.inf.key[0]).t, r.isNull); + return inf.hfields.get(r.key); + } + error("No database field '" + f.field+"'", f.expr.pos); + } + return fi; + } + + function buildCond( cond : Expr ) { + var sql = null; + var p = cond.pos; + switch( cond.expr ) { + case EObjectDecl(fl): + var first = true; + var sql = makeString("(", p); + var fields = new haxe.ds.StringMap(); + for( f in fl ) { + var fi = getField(f); + if( first ) + first = false; + else + sql = sqlAddString(sql, " AND "); + sql = sqlAddString(sql, quoteField(fi.name) + (fi.isNull ? " <=> " : " = ")); + sql = sqlAddValue(sql, f.expr, fi.t, fi.isNull); + if( fields.exists(fi.name) ) + error("Duplicate field " + fi.name, p); + else + fields.set(fi.name, true); + } + if( first ) sqlAddString(sql, "TRUE"); + sql = sqlAddString(sql, ")"); + return { sql : sql, t : DBool, n : false }; + case EParenthesis(e): + var r = buildCond(e); + r.sql = sqlAdd(makeString("(", p), r.sql, p); + r.sql = sqlAddString(r.sql, ")"); + return r; + case EBinop(op, e1, e2): + switch( op ) { + case OpAdd: + var r1 = buildCond(e1); + var r2 = buildCond(e2); + var rt = if( tryUnify(r1.t, DFloat) && tryUnify(r2.t, DFloat) ) + tryUnify(r1.t, DInt) ? tryUnify(r2.t, DInt) ? DInt : DFloat : DFloat; + else if( (tryUnify(r1.t, DText) && canStringify(r2.t)) || (tryUnify(r2.t, DText) && canStringify(r1.t)) ) + return { sql : sqlAddString(sqlAdd(sqlAddString(sqlAdd(makeString("CONCAT(",p),r1.sql,p),","),r2.sql,p),")"), t : DText, n : r1.n || r2.n } + else + error("Can't add " + typeStr(r1.t) + " and " + typeStr(r2.t), p); + return { sql : makeOp("+", r1.sql, r2.sql, p), t : rt, n : r1.n || r2.n }; + case OpBoolAnd, OpBoolOr: + var r1 = buildCond(e1); + var r2 = buildCond(e2); + unify(r1.t, DBool, e1.pos); + unify(r2.t, DBool, e2.pos); + return { sql : makeOp(op == OpBoolAnd ? " AND " : " OR ", r1.sql, r2.sql, p), t : DBool, n : false }; + case OpGte: + return buildCmp(">=", e1, e2, p); + case OpLte: + return buildCmp("<=", e1, e2, p); + case OpGt: + return buildCmp(">", e1, e2, p); + case OpLt: + return buildCmp("<", e1, e2, p); + case OpSub: + return buildNum("-", e1, e2, p); + case OpDiv: + var r = buildNum("/", e1, e2, p); + r.t = DFloat; + return r; + case OpMult: + return buildNum("*", e1, e2, p); + case OpEq, OpNotEq: + return buildEq(op == OpEq, e1, e2, p); + case OpXor: + return buildInt("^", e1, e2, p); + case OpOr: + return buildInt("|", e1, e2, p); + case OpAnd: + return buildInt("&", e1, e2, p); + case OpShr: + return buildInt(">>", e1, e2, p); + case OpShl: + return buildInt("<<", e1, e2, p); + case OpMod: + return buildNum("%", e1, e2, p); + #if (haxe_ver >= 4) + case OpIn: + var e = buildCond(e1); + var t = TPath({ + pack : [], + name : "Iterable", + params : [TPType(convertType(e.t))], + sub : null, + }); + return { sql : { expr : ECall( { expr : EField(manager, "quoteList"), pos : p }, [e.sql, { expr : ECheckType(e2,t), pos : p } ]), pos : p }, t : DBool, n : e.n }; + #end + case OpUShr, OpInterval, OpAssignOp(_), OpAssign, OpArrow: + error("Unsupported operation", p); + } + case EUnop(op, _, e): + var r = buildCond(e); + switch( op ) { + case OpNot: + var sql = makeString("!", p); + unify(r.t, DBool, e.pos); + switch( r.sql.expr ) { + case EConst(_): + default: + r.sql = sqlAddString(r.sql, ")"); + sql = sqlAddString(sql, "("); + } + return { sql : sqlAdd(sql, r.sql, p), t : DBool, n : r.n }; + case OpNegBits: + var sql = makeString("~", p); + unify(r.t, DInt, e.pos); + return { sql : sqlAdd(sql, r.sql, p), t : DInt, n : r.n }; + case OpNeg: + var sql = makeString("-", p); + unify(r.t, DFloat, e.pos); + return { sql : sqlAdd(sql, r.sql, p), t : r.t, n : r.n }; + case OpIncrement, OpDecrement: + error("Unsupported operation", p); + } + case EConst(c): + switch( c ) { + case CInt(s): return { sql : makeString(s, p), t : DInt, n : false }; + case CFloat(s): return { sql : makeString(s, p), t : DFloat, n : false }; + case CString(s): return { sql : sqlQuoteValue(cond, DText, false), t : DString(s.length), n : false }; + case CRegexp(_): error("Unsupported", p); + case CIdent(n): + if( n.charCodeAt(0) == "$".code ) { + n = n.substr(1); + var f = inf.hfields.get(n); + if( f == null ) error("Unknown database field '" + n + "'", p); + return { sql : makeString(quoteField(f.name), p), t : f.t, n : f.isNull }; + } + switch( n ) { + case "null": + return { sql : makeString("NULL", p), t : DNull, n : true }; + case "true": + return { sql : makeString("TRUE", p), t : DBool, n : false }; + case "false": + return { sql : makeString("FALSE", p), t : DBool, n : false }; + } + return buildDefault(cond); + } + case ECall(c, pl): + switch( c.expr ) { + case EConst(co): + switch(co) { + case CIdent(t): + if( t.charCodeAt(0) == '$'.code ) { + var f = g.functions.get(t.substr(1)); + if( f == null ) error("Unknown method " + t, c.pos); + if( f.params.length != pl.length ) error("Function " + f.name + " requires " + f.params.length + " parameters", p); + var parts = f.sql.split("$"); + var sql = makeString(parts[0], p); + var first = true; + var isNull = false; + for( i in 0...f.params.length ) { + var r = buildCond(pl[i]); + if( r.n ) isNull = true; + unify(r.t, f.params[i], pl[i].pos); + if( first ) + first = false; + else + sql = sqlAddString(sql, ","); + sql = sqlAdd(sql, r.sql, p); + } + sql = sqlAddString(sql, parts[1]); + // assume that for all SQL functions, a NULL parameter will make a NULL result + return { sql : sql, t : f.ret, n : isNull }; + } + default: + } + case EField(e, f): + switch( f ) { + case "like": + if( pl.length == 1 ) { + var r = buildCond(e); + var v = buildCond(pl[0]); + if( !tryUnify(r.t, DText) ) { + if( tryUnify(r.t, DBinary) ) + unify(v.t, DBinary, pl[0].pos); + else + unify(r.t, DText, e.pos); + } else + unify(v.t, DText, pl[0].pos); + return { sql : makeOp(" LIKE ", r.sql, v.sql, p), t : DBool, n : r.n || v.n }; + } + case "has": + if( pl.length == 1 ) { + var r = buildCond(e); + switch( r.t ) { + case DFlags(vals,_): + var id = makeIdent(pl[0]); + var idx = Lambda.indexOf(vals,id); + if( idx < 0 ) error("Flag should be "+vals.join(","), pl[0].pos); + return { sql : sqlAddString(r.sql, " & " + (1 << idx) + " != 0"), t : DBool, n : r.n }; + default: + } + } + } + default: + } + return buildDefault(cond); + case EField(_, _), EDisplay(_): + return buildDefault(cond); + case EIf(e, e1, e2), ETernary(e, e1, e2): + if( e2 == null ) error("If must have an else statement", p); + var r1 = buildCond(e1); + var r2 = buildCond(e2); + unify(r2.t, r1.t, e2.pos); + unify(r1.t, r2.t, e1.pos); + return { sql : { expr : EIf(e, r1.sql, r2.sql), pos : p }, t : r1.t, n : r1.n || r2.n }; + #if (haxe_ver < 4) + case EIn(e, v): + var e = buildCond(e); + var t = TPath({ + pack : [], + name : "Iterable", + params : [TPType(convertType(e.t))], + sub : null, + }); + return { sql : { expr : ECall( { expr : EField(manager, "quoteList"), pos : p }, [e.sql, { expr : ECheckType(v,t), pos : p } ]), pos : p }, t : DBool, n : e.n }; + #end + default: + return buildDefault(cond); + } + error("Unsupported expression", p); + return null; + } + + function ensureType( e : Expr, rt : RecordType, isNull : Bool ) { + var t = convertType(rt); + if (isNull) { + t = macro : Null<$t>; + } + return { expr : ECheckType(e, t), pos : e.pos }; + } + + function checkKeys( econd : Expr ) { + var p = econd.pos; + switch( econd.expr ) { + case EObjectDecl(fl): + var key = inf.key.copy(); + for( f in fl ) { + var fi = getField(f); + if( !key.remove(fi.name) ) { + if( Lambda.has(inf.key, fi.name) ) + error("Duplicate field " + fi.name, p); + else + error("Field " + f.field + " is not part of table key (" + inf.key.join(",") + ")", p); + } + f.expr = ensureType(f.expr, fi.t, fi.isNull); + } + return econd; + default: + if( inf.key.length > 1 ) + error("You can't use a single value on a table with multiple keys (" + inf.key.join(",") + ")", p); + var fi = inf.hfields.get(inf.key[0]); + return ensureType(econd, fi.t, fi.isNull); + } + } + + function orderField(e) { + switch( e.expr ) { + case EConst(c): + switch( c ) { + case CIdent(t): + if( !inf.hfields.exists(t) ) + error("Unknown database field", e.pos); + return quoteField(t); + default: + } + case EUnop(op, _, e): + if( op == OpNeg ) + return orderField(e) + " DESC"; + default: + } + error("Invalid order field", e.pos); + return null; + } + + function concatStrings( e : Expr ) { + var inf = { e : null, str : null }; + browseStrings(inf, e); + if( inf.str != null ) { + var es = { expr : EConst(CString(inf.str)), pos : e.pos }; + if( inf.e == null ) + inf.e = es; + else + inf.e = { expr : EBinop(OpAdd, inf.e, es), pos : e.pos }; + } + return inf.e; + } + + function browseStrings( inf : { e : Expr, str : String }, e : Expr ) { + switch( e.expr ) { + case EConst(c): + switch( c ) { + case CString(s): + if( inf.str == null ) + inf.str = s; + else + inf.str += s; + return; + case CInt(s), CFloat(s): + if( inf.str != null ) { + inf.str += s; + return; + } + default: + } + case EBinop(op, e1, e2): + if( op == OpAdd ) { + browseStrings(inf,e1); + browseStrings(inf,e2); + return; + } + case EIf(cond, e1, e2): + e = { expr : EIf(cond, concatStrings(e1), concatStrings(e2)), pos : e.pos }; + default: + } + if( inf.str != null ) { + e = { expr : EBinop(OpAdd, { expr : EConst(CString(inf.str)), pos : e.pos }, e), pos : e.pos }; + inf.str = null; + } + if( inf.e == null ) + inf.e = e; + else + inf.e = { expr : EBinop(OpAdd, inf.e, e), pos : e.pos }; + } + + function buildOptions( eopt : Expr ) { + var p = eopt.pos; + var opts = new haxe.ds.StringMap(); + var opt = { limit : null, orderBy : null, forceIndex : null }; + switch( eopt.expr ) { + case EObjectDecl(fields): + var limit = null; + for( o in fields ) { + if( opts.exists(o.field) ) error("Duplicate option " + o.field, p); + opts.set(o.field, true); + switch( o.field ) { + case "orderBy": + var fields = switch( o.expr.expr ) { + case EArrayDecl(vl): Lambda.array(Lambda.map(vl, orderField)); + case ECall(v, pl): + if( pl.length != 0 || !Type.enumEq(v.expr, EConst(CIdent("rand"))) ) + [orderField(o.expr)] + else + ["RAND()"]; + default: [orderField(o.expr)]; + }; + opt.orderBy = fields.join(","); + case "limit": + var limits = switch( o.expr.expr ) { + case EArrayDecl(vl): Lambda.array(Lambda.map(vl, buildDefault)); + default: [buildDefault(o.expr)]; + } + if( limits.length == 0 || limits.length > 2 ) error("Invalid limits", o.expr.pos); + var l0 = limits[0], l1 = limits[1]; + unify(l0.t, DInt, l0.sql.pos); + if( l1 != null ) + unify(l1.t, DInt, l1.sql.pos); + opt.limit = { pos : l0.sql, len : l1 == null ? null : l1.sql }; + case "forceIndex": + var fields = switch( o.expr.expr ) { + case EArrayDecl(vl): Lambda.array(Lambda.map(vl, makeIdent)); + default: [makeIdent(o.expr)]; + } + for( f in fields ) + if( !inf.hfields.exists(f) ) + error("Unknown field " + f, o.expr.pos); + var idx = fields.join(","); + if( !Lambda.exists(inf.indexes, function(i) return i.keys.join(",") == idx) && !Lambda.exists(inf.relations,function(r) return r.key == idx) ) + error("These fields are not indexed", o.expr.pos); + opt.forceIndex = idx; + default: + error("Unknown option '" + o.field + "'", p); + } + } + default: + error("Options should be { orderBy : field, limit : [a,b] }", p); + } + return opt; + } + + public static function getInfos( t : haxe.macro.Type ) { + var c = switch( t ) { + case TInst(c, _): if( c.toString() == "sys.db.Object" ) return null; c; + default: return null; + }; + return new RecordMacros(c); + } + + + #if macro + static var RTTI = false; + static var FIRST_COMPILATION = true; + + public static function addRtti() : Array { + if( RTTI ) return null; + RTTI = true; + if( FIRST_COMPILATION ) { + FIRST_COMPILATION = false; + Context.onMacroContextReused(function() { + RTTI = false; + GLOBAL = null; + return true; + }); + } + Context.getType("sys.db.RecordInfos"); + Context.onGenerate(function(types) { + for( t in types ) + switch( t ) { + case TInst(c, _): + var c = c.get(); + var cur = c.superClass; + while( cur != null ) { + if( cur.t.toString() == "sys.db.Object" ) + break; + cur = cur.t.get().superClass; + } + if( cur == null || c.meta.has(":skip") || c.meta.has("rtti") ) + continue; + var inst = getInfos(t); + var s = new haxe.Serializer(); + s.useEnumIndex = true; + s.useCache = true; + s.serialize(inst.inf); + c.meta.add("rtti", [ { expr : EConst(CString(s.toString())), pos : c.pos } ], c.pos); + default: + } + }); + Context.registerModuleReuseCall("sys.db.Manager", "sys.db.RecordMacros.addRtti()"); + return null; + } + + static function getManagerInfos( t : haxe.macro.Type, pos ) { + var param = null; + switch( t ) { + case TInst(c, p): + while( true ) { + if( c.toString() == "sys.db.Manager" ) { + param = p[0]; + break; + } + var csup = c.get().superClass; + if( csup == null ) break; + c = csup.t; + p = csup.params; + } + case TType(t, p): + if( p.length == 1 && t.toString() == "sys.db.Manager" ) + param = p[0]; + default: + } + var inst = if( param == null ) null else getInfos(param); + if( inst == null ) + Context.error("This method must be called from a specific Manager", Context.currentPos()); + inst.initManager(pos); + return inst; + } + + static function buildSQL( em : Expr, econd : Expr, prefix : String, ?eopt : Expr ) { + var pos = Context.currentPos(); + var inst = getManagerInfos(Context.typeof(em),pos); + var sql = { expr : EConst(CString(prefix + " " + inst.quoteField(inst.inf.name))), pos : econd.pos }; + var r = inst.buildCond(econd); + if( r.t != DBool ) Context.error("Expression should be a condition", econd.pos); + if( eopt != null && !Type.enumEq(eopt.expr, EConst(CIdent("null"))) ) { + var opt = inst.buildOptions(eopt); + if( opt.orderBy != null ) + r.sql = inst.sqlAddString(r.sql, " ORDER BY " + opt.orderBy); + if( opt.limit != null ) { + r.sql = inst.sqlAddString(r.sql, " LIMIT "); + r.sql = inst.sqlAdd(r.sql, opt.limit.pos, pos); + if( opt.limit.len != null ) { + r.sql = inst.sqlAddString(r.sql, ","); + r.sql = inst.sqlAdd(r.sql, opt.limit.len, pos); + } + } + if( opt.forceIndex != null ) + sql = inst.sqlAddString(sql, " FORCE INDEX (" + inst.inf.name+"_"+opt.forceIndex+")"); + } + sql = inst.sqlAddString(sql, " WHERE "); + var sql = inst.sqlAdd(sql, r.sql, sql.pos); + #if !display + sql = inst.concatStrings(sql); + #end + return sql; + } + + public static function macroGet( em : Expr, econd : Expr, elock : Expr ) { + var pos = Context.currentPos(); + var inst = getManagerInfos(Context.typeof(em),pos); + econd = inst.checkKeys(econd); + elock = defaultTrue(elock); + switch( econd.expr ) { + case EObjectDecl(_): + return { expr : ECall({ expr : EField(em,"unsafeGetWithKeys"), pos : pos },[econd,elock]), pos : pos }; + default: + return { expr : ECall({ expr : EField(em,"unsafeGet"), pos : pos },[econd,elock]), pos : pos }; + } + } + + static function defaultTrue( e : Expr ) { + return switch( e.expr ) { + case EConst(CIdent("null")): { expr : EConst(CIdent("true")), pos : e.pos }; + default: e; + } + } + + public static function macroSearch( em : Expr, econd : Expr, eopt : Expr, elock : Expr, ?single ) { + // allow both search(e,opts) and search(e,lock) + if( eopt != null && (elock == null || Type.enumEq(elock.expr, EConst(CIdent("null")))) ) { + switch( eopt.expr ) { + case EObjectDecl(_): + default: + var tmp = eopt; + eopt = elock; + elock = tmp; + } + } + var sql = buildSQL(em, econd, "SELECT * FROM", eopt); + var pos = Context.currentPos(); + var e = { expr : ECall( { expr : EField(em, "unsafeObjects"), pos : pos }, [sql,defaultTrue(elock)]), pos : pos }; + if( single ) + e = { expr : ECall( { expr : EField(e, "first"), pos : pos }, []), pos : pos }; + return e; + } + + public static function macroCount( em : Expr, econd : Expr ) { + var sql = buildSQL(em, econd, "SELECT COUNT(*) FROM"); + var pos = Context.currentPos(); + return { expr : ECall({ expr : EField(em,"unsafeCount"), pos : pos },[sql]), pos : pos }; + } + + public static function macroDelete( em : Expr, econd : Expr, eopt : Expr ) { + var sql = buildSQL(em, econd, "DELETE FROM", eopt); + var pos = Context.currentPos(); + return { expr : ECall({ expr : EField(em,"unsafeDelete"), pos : pos },[sql]), pos : pos }; + } + + static var isNeko = Context.defined("neko"); + + static function buildField( f : Field, fields : Array, ft : ComplexType, rt : ComplexType, isNull=false ) { + var p = switch( ft ) { + case TPath(p): p; + default: return; + } + if( p.params.length != 1 ) + return; + var t = switch( p.params[0] ) { + case TPExpr(_): return; + case TPType(t): t; + }; + var pos = f.pos; + switch( p.name ) { + case "SData": + f.kind = FProp("dynamic", "dynamic", rt, null); + f.meta.push( { name : ":data", params : [], pos : f.pos } ); + f.meta.push( { name : ":isVar", params : [], pos : f.pos } ); + var meta = [ { name : ":hide", params : [], pos : pos } ]; + var cache = "cache_" + f.name, + dataName = "data_" + f.name; + var ecache = { expr : EConst(CIdent(cache)), pos : pos }; + var efield = { expr : EConst(CIdent(dataName)), pos : pos }; + var fname = { expr : EConst(CString(dataName)), pos : pos }; + var get = { + args : [], + params : [], + ret : t, + // we set efield to an empty object to make sure it will be != from previous value when insert/update is triggered + expr : macro { if( $ecache == null ) { $ecache = { v : untyped manager.doUnserialize($fname, cast $efield) }; Reflect.setField(this, $fname, { } ); }; return $ecache.v; }, + }; + var set = { + args : [{ name : "_v", opt : false, type : t, value : null }], + params : [], + ret : t, + expr : macro { if( $ecache == null ) { $ecache = { v : _v }; $efield = cast {}; } else $ecache.v = _v; return _v; }, + }; + fields.push( { name : cache, pos : pos, meta : [meta[0], { name:":skip", params:[], pos:pos } ], access : [APrivate], doc : null, kind : FVar(macro : { v : $t }, null) } ); + fields.push( { name : dataName, pos : pos, meta : [meta[0], { name:":skip", params:[], pos:pos } ], access : [APrivate], doc : null, kind : FVar(macro : Dynamic, null) } ); + fields.push( { name : "get_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(get) } ); + fields.push( { name : "set_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(set) } ); + case "SEnum": + f.kind = FProp("dynamic", "dynamic", rt, null); + f.meta.push( { name : ":data", params : [], pos : f.pos } ); + var meta = [ { name : ":hide", params : [], pos : pos } ]; + var dataName = "data_" + f.name; + var efield = { expr : EConst(CIdent(dataName)), pos : pos }; + var eval = switch( t ) { + case TPath(p): + var pack = p.pack.copy(); + pack.push(p.name); + if( p.sub != null ) pack.push(p.sub); + Context.parse(pack.join("."), f.pos); + default: + Context.error("Enum parameter expected", f.pos); + } + var get = { + args : [], + params : [], + ret : t, + expr : macro return $efield == null ? null : Type.createEnumIndex($eval,cast $efield), + }; + var set = { + args : [{ name : "_v", opt : false, type : t, value : null }], + params : [], + ret : t, + expr : (Context.defined('cs') && !isNull) ? + macro { $efield = cast Type.enumIndex(_v); return _v; } : + macro { $efield = _v == null ? null : cast Type.enumIndex(_v); return _v; }, + }; + fields.push( { name : "get_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(get) } ); + fields.push( { name : "set_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(set) } ); + fields.push( { name : dataName, pos : pos, meta : [meta[0], { name:":skip", params:[], pos:pos } ], access : [APrivate], doc : null, kind : FVar(macro : Null, null) } ); + case "SNull", "Null": + buildField(f, fields, t, rt,true); + } + } + + public static function macroBuild() { + var fields = Context.getBuildFields(); + var hasManager = false; + for( f in fields ) { + var skip = false; + if( f.name == "manager") hasManager = true; + for( m in f.meta ) + switch( m.name ) { + case ":skip": + skip = true; + case ":relation": + switch( f.kind ) { + case FVar(t, _): + f.kind = FProp("dynamic", "dynamic", t); + // create compile-time getter/setter for all platforms + var relKey = null; + var relParams = []; + var lock = false; + for( p in m.params ) + switch( p.expr ) { + case EConst(c): + switch( c ) { + case CIdent(i): + relParams.push(i); + default: + } + default: + } + relKey = relParams.shift(); + for( p in relParams ) + if( p == "lock" ) + lock = true; + // we will get an error later + if( relKey == null ) + continue; + // generate get/set methods stubs + var pos = f.pos; + var ttype = t, tname; + while( true ) + switch(ttype) { + case TPath(t): + if( t.params.length == 1 && (t.name == "Null" || t.name == "SNull") ) { + ttype = switch( t.params[0] ) { + case TPType(t): t; + default: throw "assert"; + }; + continue; + } + var p = t.pack.copy(); + p.push(t.name); + if( t.sub != null ) p.push(t.sub); + tname = p.join("."); + break; + default: + Context.error("Relation type should be a type path", f.pos); + } + function e(expr) return { expr : expr, pos : pos }; + var get = { + args : [], + params : [], + ret : t, + expr : Context.parse("return untyped "+tname+".manager.__get(this,'"+f.name+"','"+relKey+"',"+lock+")",pos), + }; + var set = { + args : [{ name : "_v", opt : false, type : t, value : null }], + params : [], + ret : t, + expr : Context.parse("return untyped "+tname+".manager.__set(this,'"+f.name+"','"+relKey+"',_v)",pos), + }; + var meta = [{ name : ":hide", params : [], pos : pos }]; + f.meta.push({ name: ":isVar", params : [], pos : pos }); + fields.push({ name : "get_"+f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(get) }); + fields.push({ name : "set_"+f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(set) }); + fields.push({ name : relKey, pos : pos, meta : [{ name : ":skip", params : [], pos : pos }], access : [APrivate], doc : null, kind : FVar(macro : Dynamic) }); + default: + Context.error("Invalid relation field type", f.pos); + } + break; + default: + } + if( skip ) + continue; + switch( f.kind ) { + case FVar(t, _) | FProp('default',_,t,_): + if( t != null ) + buildField(f,fields,t,t); + default: + } + } + if( !hasManager ) { + var inst = Context.getLocalClass().get(); + if( inst.meta.has(":skip") ) + return fields; + if (!isNeko) + { + var iname = { expr:EConst(CIdent(inst.name)), pos: inst.pos }; + var getM = { + args : [], + params : [], + ret : macro : sys.db.Manager, + expr : macro return $iname.manager + }; + fields.push({ name: "__getManager", meta : [], access : [APrivate,AOverride], doc : null, kind : FFun(getM), pos : inst.pos }); + } + var p = inst.pos; + var tinst = TPath( { pack : inst.pack, name : inst.name, sub : null, params : [] } ); + var path = inst.pack.copy().concat([inst.name]).join("."); + var enew = { expr : ENew( { pack : ["sys", "db"], name : "Manager", sub : null, params : [TPType(tinst)] }, [Context.parse(path, p)]), pos : p } + fields.push({ name : "manager", meta : [], kind : FVar(null,enew), doc : null, access : [AStatic,APublic], pos : p }); + } + return fields; + } + + #end + +} diff --git a/src/sys/db/TableCreate.hx b/src/sys/db/TableCreate.hx new file mode 100644 index 0000000..63f2479 --- /dev/null +++ b/src/sys/db/TableCreate.hx @@ -0,0 +1,109 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package sys.db; +import sys.db.RecordInfos; + +class TableCreate { + + static function autoInc( dbName ) { + // on SQLite, autoincrement is necessary to be primary key as well + return dbName == "SQLite" ? "PRIMARY KEY AUTOINCREMENT" : "AUTO_INCREMENT"; + } + + public static function getTypeSQL( t : RecordType, dbName : String ) { + return switch( t ) { + case DId: "INTEGER "+autoInc(dbName); + case DUId: "INTEGER UNSIGNED "+autoInc(dbName); + case DInt, DEncoded: "INTEGER"; + case DUInt: "INTEGER UNSIGNED"; + case DTinyInt: "TINYINT"; + case DTinyUInt, DEnum(_): "TINYINT UNSIGNED"; + case DSmallInt: "SMALLINT"; + case DSmallUInt: "SMALLINT UNSIGNED"; + case DMediumInt: "MEDIUMINT"; + case DMediumUInt: "MEDIUMINT UNSIGNED"; + case DSingle: "FLOAT"; + case DFloat: "DOUBLE"; + case DBool: "TINYINT(1)"; + case DString(n): "VARCHAR("+n+")"; + case DDate: "DATE"; + case DDateTime: "DATETIME"; + case DTimeStamp: "TIMESTAMP DEFAULT 0"; + case DTinyText: "TINYTEXT"; + case DSmallText: "TEXT"; + case DText, DSerialized: "MEDIUMTEXT"; + case DSmallBinary: "BLOB"; + case DBinary, DNekoSerialized, DData: "MEDIUMBLOB"; + case DLongBinary: "LONGBLOB"; + case DBigInt: "BIGINT"; + case DBigId: "BIGINT "+autoInc(dbName); + case DBytes(n): "BINARY(" + n + ")"; + case DFlags(fl, auto): getTypeSQL(auto ? (fl.length <= 8 ? DTinyUInt : (fl.length <= 16 ? DSmallUInt : (fl.length <= 24 ? DMediumUInt : DInt))) : DInt, dbName); + case DNull, DInterval: throw "assert"; + }; + } + + public static function create( manager : sys.db.Manager, ?engine ) { + function quote(v:String):String { + return untyped manager.quoteField(v); + } + var cnx : Connection = untyped manager.getCnx(); + if( cnx == null ) + throw "SQL Connection not initialized on Manager"; + var dbName = cnx.dbName(); + var infos = manager.dbInfos(); + var sql = "CREATE TABLE " + quote(infos.name) + " ("; + var decls = []; + var hasID = false; + for( f in infos.fields ) { + switch( f.t ) { + case DId: + hasID = true; + case DUId, DBigId: + hasID = true; + if( dbName == "SQLite" ) + throw "S" + Std.string(f.t).substr(1)+" is not supported by " + dbName + " : use SId instead"; + default: + } + decls.push(quote(f.name)+" "+getTypeSQL(f.t,dbName)+(f.isNull ? "" : " NOT NULL")); + } + if( dbName != "SQLite" || !hasID ) + decls.push("PRIMARY KEY ("+Lambda.map(infos.key,quote).join(",")+")"); + sql += decls.join(","); + sql += ")"; + if( engine != null ) + sql += "ENGINE="+engine; + cnx.request(sql); + } + + public static function exists( manager : sys.db.Manager ) : Bool { + var cnx : Connection = untyped manager.getCnx(); + if( cnx == null ) + throw "SQL Connection not initialized on Manager"; + try { + cnx.request("SELECT * FROM `"+manager.dbInfos().name+"` LIMIT 1"); + return true; + } catch( e : Dynamic ) { + return false; + } + } +} diff --git a/src/sys/db/Transaction.hx b/src/sys/db/Transaction.hx new file mode 100644 index 0000000..44a3461 --- /dev/null +++ b/src/sys/db/Transaction.hx @@ -0,0 +1,70 @@ +/* + * Copyright (C)2005-2017 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +class Transaction { + + public static function isDeadlock(e : Dynamic) { + return Std.is(e,String) && ~/try restarting transaction/.match(e); + } + + private static function runMainLoop(mainFun,logError,count) { + try { + mainFun(); + } catch( e : Dynamic ) { + if( count > 0 && isDeadlock(e) ) { + Manager.cleanup(); + Manager.cnx.rollback(); // should be already done, but in case... + Manager.cnx.startTransaction(); + runMainLoop(mainFun,logError,count-1); + return; + } + if( logError == null ) { + Manager.cnx.rollback(); + #if neko + neko.Lib.rethrow(e); + #else + throw e; + #end + } + logError(e); // should ROLLBACK if needed + } + } + + public static function main( cnx, mainFun : Void -> Void, ?logError : Dynamic -> Void ) { + Manager.initialize(); + Manager.cnx = cnx; + Manager.cnx.startTransaction(); + runMainLoop(mainFun,logError,3); + try { + Manager.cnx.commit(); + } catch( e : String ) { + // sqlite can have errors on commit + if( ~/Database is busy/.match(e) ) + logError(e); + } + Manager.cnx.close(); + Manager.cnx = null; + Manager.cleanup(); + } + +} diff --git a/src/sys/db/Types.hx b/src/sys/db/Types.hx new file mode 100644 index 0000000..c6bbe33 --- /dev/null +++ b/src/sys/db/Types.hx @@ -0,0 +1,127 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +// basic types + +/** int with auto increment **/ +@:noPackageRestrict +typedef SId = Null + +/** int unsigned with auto increment **/ +typedef SUId = Null + +/** big int with auto increment **/ +typedef SBigId = Null + +typedef SInt = Null + +typedef SUInt = Null + +typedef SBigInt = Null + +/** single precision float **/ +typedef SSingle = Null + +/** double precision float **/ +typedef SFloat = Null + +/** use `tinyint(1)` to distinguish with int **/ +typedef SBool = Null + +/** same as `varchar(n)` **/ +typedef SString = String + +/** date only, use `SDateTime` for date+time **/ +typedef SDate = Date + +/** mysql DateTime **/ +typedef SDateTime = Date + +/** mysql Timestamp **/ +typedef STimeStamp = Date + +/** TinyText (up to 255 bytes) **/ +typedef STinyText = String + +/** Text (up to 64KB) **/ +typedef SSmallText = String + +/** MediumText (up to 24MB) **/ +typedef SText = String + +/** Blob type (up to 64KB) **/ +typedef SSmallBinary = haxe.io.Bytes + +/** LongBlob type (up to 4GB) **/ +typedef SLongBinary = haxe.io.Bytes + +/** MediumBlob type (up to 24MB) **/ +typedef SBinary = haxe.io.Bytes + +/** same as binary(n) **/ +typedef SBytes = haxe.io.Bytes + +/** one byte signed `-128...127` **/ +typedef STinyInt = Null + +/** two bytes signed `-32768...32767` **/ +typedef SSmallInt = Null + +/** three bytes signed `-8388608...8388607` **/ +typedef SMediumInt = Null + +/** one byte `0...255` **/ +typedef STinyUInt = Null + +/** two bytes `0...65535` **/ +typedef SSmallUInt = Null + +/** three bytes `0...16777215` **/ +typedef SMediumUInt = Null + +// extra + +/** specify that this field is nullable **/ +typedef SNull = Null + +/** specify that the integer use custom encoding **/ +typedef SEncoded = Null + +/** Haxe Serialized string **/ +typedef SSerialized = String + +/** native neko serialized bytes **/ +typedef SNekoSerialized = haxe.io.Bytes + +/** a set of bitflags of different enum values **/ +typedef SFlags = Null> + +/** same as `SFlags` but will adapt the storage size to the number of flags **/ +typedef SSmallFlags = SFlags; + +/** allow to store any value in serialized form **/ +typedef SData = Null + +/** allow to store an enum value that does not have parameters as a simple int **/ +typedef SEnum = Null + diff --git a/test.hxml b/test.hxml new file mode 100644 index 0000000..7f81540 --- /dev/null +++ b/test.hxml @@ -0,0 +1,5 @@ +-cp src +-cp test +-lib hexunit:0.35.0 +-main Main +-neko test.n diff --git a/test/Main.hx b/test/Main.hx new file mode 100644 index 0000000..e782c6c --- /dev/null +++ b/test/Main.hx @@ -0,0 +1,21 @@ +import hex.unittest.notifier.*; +import hex.unittest.runner.*; + +using Lambda; + +class Main +{ + static function main() { + var arg = Sys.args()[0]; + var mysqlConnection = (arg != null && arg.substr(0,8)=="mysql://") ? arg : null; + + var emu = new ExMachinaUnitCore(); + emu.addListener(new ConsoleNotifier(false)); + emu.addListener(new ExitingNotifier()); + if(mysqlConnection!=null) emu.addTest(MySQLTest); + emu.addTest(SQLiteTest); + emu.run(); + } + + +} diff --git a/test/MySQLTest.hx b/test/MySQLTest.hx new file mode 100644 index 0000000..a9b646c --- /dev/null +++ b/test/MySQLTest.hx @@ -0,0 +1,588 @@ +import sys.db.*; +import sys.db.Types; +import haxe.io.Bytes; +import haxe.EnumFlags; +import MySpodClass; +import hex.unittest.assertion.Assert; +import hex.unittest.notifier.*; +import hex.unittest.runner.*; + +using Lambda; + +class MySQLTest +{ + + @Before + public function before() + { + connectDb(); + + try Manager.cnx.request('DROP TABLE MySpodClass') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE OtherSpodClass') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE NullableSpodClass') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE ClassWithStringId') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE ClassWithStringIdRef') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE IssueC3828') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE Issue6041Table') catch(e:Dynamic) {} + TableCreate.create(MySpodClass.manager); + TableCreate.create(OtherSpodClass.manager); + TableCreate.create(NullableSpodClass.manager); + TableCreate.create(ClassWithStringId.manager); + TableCreate.create(ClassWithStringIdRef.manager); + TableCreate.create(IssueC3828.manager); + TableCreate.create(Issue6041Table.manager); + + Manager.cleanup(); + } + + @After + public function after() + { + Manager.cnx.close(); + } + + function connectDb() { + var dbstr = Sys.args()[0]; + var dbreg = ~/([^:]+):\/\/([^:]+):([^@]*?)@([^:]+)(:[0-9]+)?\/(.*?)$/; + if( !dbreg.match(dbstr) ) + throw "Configuration requires a valid database attribute, format is : mysql://user:password@host:port/dbname"; + var port = dbreg.matched(5); + var dbparams = { + user:dbreg.matched(2), + pass:dbreg.matched(3), + host:dbreg.matched(4), + port:port == null ? 3306 : Std.parseInt(port.substr(1)), + database:dbreg.matched(6), + socket:null + }; + + sys.db.Manager.cnx = sys.db.Mysql.connect(dbparams); + sys.db.Manager.initialize(); + } + + function getDefaultClass() + { + var scls = new MySpodClass(); + scls.int = 1; + scls.double = 2.0; + scls.boolean = true; + scls.string = "some string"; + scls.date = new Date(2012, 7, 30, 0, 0, 0); + scls.abstractType = "other string"; + + var bytes = Bytes.ofString("\x01\n\r'\x02"); + scls.binary = bytes; + scls.enumFlags = EnumFlags.ofInt(0); + scls.enumFlags.set(FirstValue); + scls.enumFlags.set(ThirdValue); + scls.bytes = Bytes.ofString("\000a"); + + scls.data = [new ComplexClass( { name:"test", array:["this", "is", "a", "test"] } )]; + scls.anEnum = SecondValue; + + return scls; + } + + function getDefaultNull() { + var scls = new NullableSpodClass(); + scls.int = 1; + scls.double = 2.0; + scls.boolean = true; + scls.string = "some string"; + scls.date = new Date(2012, 7, 30, 0, 0, 0); + scls.abstractType = "other string"; + + var bytes = Bytes.ofString("\x01\n\r'\x02"); + scls.binary = bytes; + scls.enumFlags = EnumFlags.ofInt(0); + scls.enumFlags.set(FirstValue); + scls.enumFlags.set(ThirdValue); + + scls.data = [new ComplexClass( { name:"test", array:["this", "is", "a", "test"] } )]; + scls.anEnum = SecondValue; + return scls; + } + + private function getNull():Null { + return null; + } + + #if !php + //TODO : these tests fail with PHP 7 and haxe 3.4.7 + @Test + public function testNull() { + var n1 = getDefaultNull(); + n1.insert(); + var n2 = new NullableSpodClass(); + n2.insert(); + var id = n2.theId; + + n1 = null; n2 = null; + Manager.cleanup(); + + var nullVal = getNull(); + inline function checkReq(lst:List, ?nres=1, ?pos:haxe.PosInfos) { + Assert.equals(nres, lst.length, null, pos); + if (lst.length == 1) { + Assert.equals(id, lst.first().theId, null, pos); + } + } + + checkReq(NullableSpodClass.manager.search($relationNullable == null), 2); + checkReq(NullableSpodClass.manager.search($data == null)); + checkReq(NullableSpodClass.manager.search($anEnum == null)); + + checkReq(NullableSpodClass.manager.search($int == null)); + checkReq(NullableSpodClass.manager.search($double == null)); + checkReq(NullableSpodClass.manager.search($boolean == null)); + checkReq(NullableSpodClass.manager.search($string == null)); + checkReq(NullableSpodClass.manager.search($date == null)); + checkReq(NullableSpodClass.manager.search($binary == null)); + checkReq(NullableSpodClass.manager.search($abstractType == null)); + + checkReq(NullableSpodClass.manager.search($enumFlags == null)); + + + var relationNullable:Null = getNull(); + checkReq(NullableSpodClass.manager.search($relationNullable == relationNullable), 2); + var data:Null = getNull(); + checkReq(NullableSpodClass.manager.search($data == data)); + var anEnum:Null> = getNull(); + checkReq(NullableSpodClass.manager.search($anEnum == anEnum)); + + var int:Null = getNull(); + checkReq(NullableSpodClass.manager.search($int == int)); + var double:Null = getNull(); + checkReq(NullableSpodClass.manager.search($double == double)); + var boolean:Null = getNull(); + checkReq(NullableSpodClass.manager.search($boolean == boolean)); + var string:SNull> = getNull(); + checkReq(NullableSpodClass.manager.search($string == string)); + var date:SNull = getNull(); + checkReq(NullableSpodClass.manager.search($date == date)); + var binary:SNull = getNull(); + checkReq(NullableSpodClass.manager.search($binary == binary)); + var abstractType:SNull = getNull(); + checkReq(NullableSpodClass.manager.search($abstractType == abstractType)); + + for (val in NullableSpodClass.manager.all()) { + val.delete(); + } + } + + + + @Test + public function testIssue3828() + { + var u1 = new IssueC3828(); + u1.insert(); + var u2 = new IssueC3828(); + u2.refUser = u1; + u2.insert(); + var u1id = u1.id, u2id = u2.id; + u1 = null; u2 = null; + Manager.cleanup(); + + var u1 = IssueC3828.manager.get(u1id); + var u2 = IssueC3828.manager.search($refUser == u1).first(); + Assert.equals(u1id, u1.id); + Assert.equals(u2id, u2.id); + } + + @Test + public function testIssue6041() + { + var item = new Issue6041Table(); + item.insert(); + var result = Manager.cnx.request('SELECT * FROM Issue6041Table LIMIT 1'); + var amount = 1; + for(row in result) { + Assert.isFalse(--amount < 0, "Invalid amount of rows in result"); + } + Assert.equals(0, amount); + } + + @Test + public function testStringIdRel() + { + var s = new ClassWithStringId(); + s.name = "first"; + s.field = 1; + s.insert(); + var v1 = new ClassWithStringIdRef(); + v1.ref = s; + v1.insert(); + var v2 = new ClassWithStringIdRef(); + v2.ref = s; + v2.insert(); + + s = new ClassWithStringId(); + s.name = "second"; + s.field = 2; + s.insert(); + v1 = new ClassWithStringIdRef(); + v1.ref = s; + v1.insert(); + s = null; v1 = null; v2 = null; + Manager.cleanup(); + + var first = ClassWithStringId.manager.search($name == "first"); + Assert.equals(1, first.length); + var first = first.first(); + Assert.equals(1, first.field); + var frel = ClassWithStringIdRef.manager.search($ref == first); + Assert.equals(2, frel.length); + for (rel in frel) + Assert.equals(first, rel.ref); + var frel2 = ClassWithStringIdRef.manager.search($ref_id == "first"); + Assert.equals(2, frel2.length); + for (rel in frel2) + Assert.equals(first, rel.ref); + + var second = ClassWithStringId.manager.search($name == "second"); + Assert.equals(1, second.length); + var second = second.first(); + Assert.equals(2, second.field); + var srel = ClassWithStringIdRef.manager.search($ref == second); + Assert.equals(1, srel.length); + for (rel in srel) + Assert.equals(second, rel.ref); + + Assert.equals(-1, frel.array().indexOf(srel.first())); + second.delete(); + for (r in srel) r.delete(); + first.delete(); + for (r in frel) r.delete(); + } + + @Test + public function testEnum() + { + var c1 = new OtherSpodClass("first spod"); + c1.insert(); + var c2 = new OtherSpodClass("second spod"); + c2.insert(); + + var scls = getDefaultClass(); + var scls1 = scls; + scls.relation = c1; + scls.insert(); + var id1 = scls.theId; + scls = getDefaultClass(); + scls.relation = c1; + scls.insert(); + + scls1.next = scls; + scls1.update(); + + var id2 = scls.theId; + scls = getDefaultClass(); + scls.relation = c1; + scls.next = scls1; + scls.anEnum = FirstValue; + scls.insert(); + var id3 = scls.theId; + scls = null; + + Manager.cleanup(); + var r1s = [ for (c in MySpodClass.manager.search($anEnum == SecondValue,{orderBy:theId})) c.theId ]; + Assert.deepEquals(r1s, [id1, id2]); + var r2s = MySpodClass.manager.search($anEnum == FirstValue); + Assert.equals(1, r2s.length); + Assert.equals(id3, r2s.first().theId); + Assert.equals(id1, r2s.first().next.theId); + Assert.equals(id2, r2s.first().next.next.theId); + + var fv = getSecond(); + var r1s = [ for (c in MySpodClass.manager.search($anEnum == fv,{orderBy:theId})) c.theId ]; + Assert.deepEquals(r1s, [id1, id2]); + var r2s = MySpodClass.manager.search($anEnum == getFirst()); + Assert.equals(1, r2s.length); + Assert.equals(id3, r2s.first().theId); + + var ids = [id1,id2,id3]; + var s = [ for (c in MySpodClass.manager.search( $anEnum == SecondValue || ($theId in ids) )) c.theId ]; + s.sort(Reflect.compare); + Assert.deepEquals(s, [id1, id2, id3]); + + r2s.first().delete(); + for (v in MySpodClass.manager.search($anEnum == fv)) v.delete(); + } + + public function getFirst() + { + return FirstValue; + } + + public function getSecond() + { + return SecondValue; + } + + @Test + public function testUpdate() + { + var c1 = new OtherSpodClass("first spod"); + c1.insert(); + var c2 = new OtherSpodClass("second spod"); + c2.insert(); + var scls = getDefaultClass(); + scls.relation = c1; + scls.relationNullable = c2; + scls.insert(); + + var id = scls.theId; + + //if no change made, update should return nothing + Assert.isNull(untyped MySpodClass.manager.getUpdateStatement(scls)); + Manager.cleanup(); + scls = MySpodClass.manager.get(id); + Assert.isNull(untyped MySpodClass.manager.getUpdateStatement(scls)); + scls.delete(); + + //try now with null SData and null relation + var scls = new NullableSpodClass(); + scls.insert(); + + var id = scls.theId; + + //if no change made, update should return nothing + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Manager.cleanup(); + scls = NullableSpodClass.manager.get(id); + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Assert.isNull(scls.data); + Assert.isNull(scls.relationNullable); + Assert.isNull(scls.abstractType); + Assert.isNull(scls.anEnum); + scls.delete(); + + //same thing with explicit null set + var scls = new NullableSpodClass(); + scls.data = null; + scls.relationNullable = null; + scls.abstractType = null; + scls.anEnum = null; + scls.insert(); + + var id = scls.theId; + + //if no change made, update should return nothing + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Manager.cleanup(); + scls = NullableSpodClass.manager.get(id); + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Assert.isNull(scls.data); + Assert.isNull(scls.relationNullable); + Assert.isNull(scls.abstractType); + Assert.isNull(scls.anEnum); + Manager.cleanup(); + + scls = new NullableSpodClass(); + scls.theId = id; + Assert.isNotNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + + scls.delete(); + } + + @Test + public function testSpodTypes() + { + var c1 = new OtherSpodClass("first spod"); + c1.insert(); + var c2 = new OtherSpodClass("second spod"); + c2.insert(); + + var scls = getDefaultClass(); + + scls.relation = c1; + scls.relationNullable = c2; + scls.insert(); + + //after inserting, id must be filled + Assert.notEquals(0, scls.theId, pos()); + Assert.isNotNull(scls.theId); + var theid = scls.theId; + + c1 = c2 = null; + Manager.cleanup(); + + var cls1 = MySpodClass.manager.get(theid); + Assert.isNotNull(cls1, pos()); + //after Manager.cleanup(), the instances should be different + Assert.isFalse(cls1 == scls, pos()); + scls = null; + + Assert.isInstanceOf(cls1.int, Int, pos()); + Assert.equals(1, cls1.int, pos()); + Assert.isInstanceOf(cls1.double, Float, pos()); + Assert.equals(2.0, cls1.double, pos()); + Assert.isInstanceOf(cls1.boolean, Bool, pos()); + Assert.isTrue(cls1.boolean, pos()); + Assert.isInstanceOf(cls1.string, String, pos()); + Assert.equals("some string", cls1.string, pos()); + Assert.isInstanceOf(cls1.abstractType, String, pos()); + Assert.equals("other string", cls1.abstractType.get(), pos()); + Assert.isNotNull(cls1.date, pos()); + Assert.isInstanceOf(cls1.date, Date, pos()); + Assert.equals(new Date(2012, 7, 30, 0, 0, 0).getTime(), cls1.date.getTime(), pos()); + + Assert.isInstanceOf(cls1.binary, Bytes, pos()); + Assert.equals(0, cls1.binary.compare(Bytes.ofString("\x01\n\r'\x02")), pos()); + Assert.isTrue(cls1.enumFlags.has(FirstValue), pos()); + Assert.isFalse(cls1.enumFlags.has(SecondValue), pos()); + Assert.isTrue(cls1.enumFlags.has(ThirdValue), pos()); + + Assert.isInstanceOf(cls1.data, Array, pos()); + Assert.isInstanceOf(cls1.data[0], ComplexClass, pos()); + + Assert.equals("test", cls1.data[0].val.name, pos()); + Assert.equals(4, cls1.data[0].val.array.length, pos()); + Assert.equals("is", cls1.data[0].val.array[1], pos()); + + Assert.equals("first spod", cls1.relation.name, pos()); + Assert.equals("second spod", cls1.relationNullable.name, pos()); + + Assert.equals(SecondValue, cls1.anEnum, pos()); + Assert.isInstanceOf(cls1.anEnum, SpodEnum, pos()); + + Assert.equals("\000a", cls1.bytes.toString()); + + Assert.equals(MySpodClass.manager.select($anEnum == SecondValue), cls1, pos()); + + //test create a new class + var scls = getDefaultClass(); + + c1 = new OtherSpodClass("third spod"); + c1.insert(); + + scls.relation = c1; + scls.insert(); + + scls = cls1 = null; + Manager.cleanup(); + + Assert.equals(2, MySpodClass.manager.all().length, pos()); + var req = MySpodClass.manager.search({ relation: OtherSpodClass.manager.select({ name:"third spod"} ) }); + Assert.equals(1, req.length, pos()); + scls = req.first(); + + scls.relation.name = "Test"; + scls.relation.update(); + + Assert.isNull(OtherSpodClass.manager.select({ name:"third spod" }), pos()); + + for (c in MySpodClass.manager.all()) + c.delete(); + for (c in OtherSpodClass.manager.all()) + c.delete(); + + //issue #3598 + var inexistent = MySpodClass.manager.get(1000,false); + Assert.isNull(inexistent); + } + + @Test + public function testDateQuery() + { + var other1 = new OtherSpodClass("required field"); + other1.insert(); + + var now = Date.now(); + var c1 = getDefaultClass(); + c1.relation = other1; + c1.date = now; + c1.insert(); + + var c2 = getDefaultClass(); + c2.relation = other1; + c2.date = DateTools.delta(now, DateTools.hours(1)); + c2.insert(); + + var q = MySpodClass.manager.search($date > now); + Assert.equals(1, q.length, pos()); + Assert.equals(c2, q.first(), pos()); + + q = MySpodClass.manager.search($date == now); + Assert.equals(1, q.length, pos()); + Assert.equals(c1, q.first(), pos()); + + q = MySpodClass.manager.search($date >= now); + Assert.equals(2, q.length, pos()); + Assert.equals(c1, q.first(), pos()); + + q = MySpodClass.manager.search($date >= DateTools.delta(now, DateTools.hours(2))); + Assert.equals(0, q.length, pos()); + Assert.isNull(q.first(), pos()); + + c1.delete(); + c2.delete(); + other1.delete(); + } + + + @Test + public function testData() + { + var other1 = new OtherSpodClass("required field"); + other1.insert(); + + var c1 = getDefaultClass(); + c1.relation = other1; + c1.insert(); + + Assert.equals(1, c1.data.length, pos()); + c1.data.pop(); + c1.update(); + + Manager.cleanup(); + c1 = null; + + c1 = MySpodClass.manager.select($relation == other1); + Assert.equals(0, c1.data.length, pos()); + c1.data.push(new ComplexClass({ name: "test1", array:["complex","field"] })); + c1.data.push(null); + Assert.equals(2, c1.data.length, pos()); + c1.update(); + + Manager.cleanup(); + c1 = null; + + c1 = MySpodClass.manager.select($relation == other1); + Assert.equals(2, c1.data.length, pos()); + Assert.equals("test1", c1.data[0].val.name, pos()); + Assert.equals(2, c1.data[0].val.array.length, pos()); + Assert.equals("complex", c1.data[0].val.array[0], pos()); + Assert.isNull(c1.data[1], pos()); + + c1.delete(); + other1.delete(); + } + #end + + /** + Check that relations are not affected by the analyzer + + See: #6 and HaxeFoundation/haxe#6048 + + The way the analyzer transforms the expression (to prevent potential + side-effects) might change the context where `untyped __this__` is + evaluated. + **/ + @Test + public function testIssue6() + { + var parent = new MySpodClass(); + parent.relation = new OtherSpodClass("i"); + + Assert.isNotNull(parent.relation); + Assert.equals("i", parent.relation.name); + } + + private function pos(?p:haxe.PosInfos):haxe.PosInfos + { + p.fileName = p.fileName + "(" + Manager.cnx.dbName() +")"; + return p; + } +} diff --git a/test/MySpodClass.hx b/test/MySpodClass.hx new file mode 100644 index 0000000..eb5cec1 --- /dev/null +++ b/test/MySpodClass.hx @@ -0,0 +1,126 @@ +import sys.db.Object; +import sys.db.Types; + +@:keep class MySpodClass extends Object +{ + public var theId:SId; + public var int:SInt; + public var double:SFloat; + public var boolean:SBool; + public var string:SString<255>; + public var date:SDateTime; + public var binary:SBinary; + public var abstractType:AbstractSpodTest; + + public var nullInt:SNull; + public var enumFlags:SFlags; + + @:relation(rid) public var relation:OtherSpodClass; + @:relation(rnid) public var relationNullable:Null; + @:relation(spid) public var next:Null; + + public var data:SData>; + public var anEnum:SEnum; + public var bytes:SBytes<2>; +} + +@:keep class NullableSpodClass extends Object +{ + public var theId:SId; + @:relation(rnid) public var relationNullable:Null; + public var data:Null>>; + public var anEnum:Null>; + + public var int:SNull; + public var double:SNull; + public var boolean:SNull; + public var string:SNull>; + public var date:SNull; + public var binary:SNull; + public var abstractType:SNull>; + + public var nullInt:SNull; + public var enumFlags:SNull>; +} + +@:keep class ComplexClass +{ + public var val : { name:String, array:Array }; + + public function new(val) + { + this.val = val; + } +} + +@:id(theid) @:keep class OtherSpodClass extends Object +{ + public var theid:SInt; + public var name:SString<255>; + + public function new(name:String) + { + super(); + this.name =name; + } +} + +@:keep enum SpodEnum +{ + FirstValue; + SecondValue; + ThirdValue; +} + +abstract AbstractSpodTest(A) from A +{ + public function get():A + { + return this; + } +} + +@:id(name) + @:keep class ClassWithStringId extends Object +{ + public var name:SString<255>; + public var field:SInt; +} + +@:keep class ClassWithStringIdRef extends Object +{ + public var id:SId; + @:relation(ref_id) public var ref:ClassWithStringId; +} + + +//issue #3828 +@:keep @:skip class BaseIssueC3828 extends sys.db.Object { + public var id : SInt; + @:relation(ruid) + public var refUser : SNull; +} + +@:keep class IssueC3828 extends BaseIssueC3828 { +} + +@:keep class Issue6041Table extends Object { + public var id:SInt = 0; +} + +// issue # +class TLazyIssueFoo extends sys.db.Object { + public var id:SId; + @:relation(bid) public var bar:TLazyIssueBar; + + public function new(bar:TLazyIssueBar) + { + var lastFoo = TLazyIssueFoo.manager.select($bar == bar, { orderBy : -id, limit : 1 }, false); + super(); + } +} +class TLazyIssueBar extends sys.db.Object { + public var id:SId; + public var initialized:SString<255> = "bar"; +} + diff --git a/test/SQLiteTest.hx b/test/SQLiteTest.hx new file mode 100644 index 0000000..ba331ad --- /dev/null +++ b/test/SQLiteTest.hx @@ -0,0 +1,569 @@ +import sys.db.*; +import sys.db.Types; +import haxe.io.Bytes; +import haxe.EnumFlags; +import MySpodClass; +import hex.unittest.assertion.Assert; +import hex.unittest.notifier.*; +import hex.unittest.runner.*; + +using Lambda; + +class SQLiteTest +{ + + @Before + public function before() + { + Manager.initialize(); + Manager.cnx = sys.db.Sqlite.open("test.sqlite"); + try Manager.cnx.request('DROP TABLE MySpodClass') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE OtherSpodClass') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE NullableSpodClass') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE ClassWithStringId') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE ClassWithStringIdRef') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE IssueC3828') catch(e:Dynamic) {} + try Manager.cnx.request('DROP TABLE Issue6041Table') catch(e:Dynamic) {} + TableCreate.create(MySpodClass.manager); + TableCreate.create(OtherSpodClass.manager); + TableCreate.create(NullableSpodClass.manager); + TableCreate.create(ClassWithStringId.manager); + TableCreate.create(ClassWithStringIdRef.manager); + TableCreate.create(IssueC3828.manager); + TableCreate.create(Issue6041Table.manager); + + Manager.cleanup(); + } + + @After + public function after() + { + Manager.cnx.close(); + } + + function getDefaultClass() + { + var scls = new MySpodClass(); + scls.int = 1; + scls.double = 2.0; + scls.boolean = true; + scls.string = "some string"; + scls.date = new Date(2012, 7, 30, 0, 0, 0); + scls.abstractType = "other string"; + + var bytes = Bytes.ofString("\x01\n\r'\x02"); + scls.binary = bytes; + scls.enumFlags = EnumFlags.ofInt(0); + scls.enumFlags.set(FirstValue); + scls.enumFlags.set(ThirdValue); + scls.bytes = Bytes.ofString("\000a"); + + scls.data = [new ComplexClass( { name:"test", array:["this", "is", "a", "test"] } )]; + scls.anEnum = SecondValue; + + return scls; + } + + function getDefaultNull() { + var scls = new NullableSpodClass(); + scls.int = 1; + scls.double = 2.0; + scls.boolean = true; + scls.string = "some string"; + scls.date = new Date(2012, 7, 30, 0, 0, 0); + scls.abstractType = "other string"; + + var bytes = Bytes.ofString("\x01\n\r'\x02"); + scls.binary = bytes; + scls.enumFlags = EnumFlags.ofInt(0); + scls.enumFlags.set(FirstValue); + scls.enumFlags.set(ThirdValue); + + scls.data = [new ComplexClass( { name:"test", array:["this", "is", "a", "test"] } )]; + scls.anEnum = SecondValue; + return scls; + } + + @Test + public function testNull() { + var n1 = getDefaultNull(); + n1.insert(); + var n2 = new NullableSpodClass(); + n2.insert(); + var id = n2.theId; + + n1 = null; n2 = null; + Manager.cleanup(); + + var nullVal = getNull(); + inline function checkReq(lst:List, ?nres=1, ?pos:haxe.PosInfos) { + Assert.equals(nres, lst.length, null, pos); + if (lst.length == 1) { + Assert.equals(id, lst.first().theId, null, pos); + } + } + + checkReq(NullableSpodClass.manager.search($relationNullable == null), 2); + checkReq(NullableSpodClass.manager.search($data == null)); + checkReq(NullableSpodClass.manager.search($anEnum == null)); + + checkReq(NullableSpodClass.manager.search($int == null)); + checkReq(NullableSpodClass.manager.search($double == null)); + checkReq(NullableSpodClass.manager.search($boolean == null)); + checkReq(NullableSpodClass.manager.search($string == null)); + checkReq(NullableSpodClass.manager.search($date == null)); + checkReq(NullableSpodClass.manager.search($binary == null)); + checkReq(NullableSpodClass.manager.search($abstractType == null)); + + checkReq(NullableSpodClass.manager.search($enumFlags == null)); + + + var relationNullable:Null = getNull(); + checkReq(NullableSpodClass.manager.search($relationNullable == relationNullable), 2); + var data:Null = getNull(); + checkReq(NullableSpodClass.manager.search($data == data)); + var anEnum:Null> = getNull(); + checkReq(NullableSpodClass.manager.search($anEnum == anEnum)); + + var int:Null = getNull(); + checkReq(NullableSpodClass.manager.search($int == int)); + var double:Null = getNull(); + checkReq(NullableSpodClass.manager.search($double == double)); + var boolean:Null = getNull(); + checkReq(NullableSpodClass.manager.search($boolean == boolean)); + var string:SNull> = getNull(); + checkReq(NullableSpodClass.manager.search($string == string)); + var date:SNull = getNull(); + checkReq(NullableSpodClass.manager.search($date == date)); + var binary:SNull = getNull(); + checkReq(NullableSpodClass.manager.search($binary == binary)); + var abstractType:SNull = getNull(); + checkReq(NullableSpodClass.manager.search($abstractType == abstractType)); + + for (val in NullableSpodClass.manager.all()) { + val.delete(); + } + } + + private function getNull():Null { + return null; + } + + @Test + public function testIssue3828() + { + var u1 = new IssueC3828(); + u1.insert(); + var u2 = new IssueC3828(); + u2.refUser = u1; + u2.insert(); + var u1id = u1.id, u2id = u2.id; + u1 = null; u2 = null; + Manager.cleanup(); + + var u1 = IssueC3828.manager.get(u1id); + var u2 = IssueC3828.manager.search($refUser == u1).first(); + Assert.equals(u1id, u1.id); + Assert.equals(u2id, u2.id); + } + + @Test + public function testIssue6041() + { + var item = new Issue6041Table(); + item.insert(); + var result = Manager.cnx.request('SELECT * FROM Issue6041Table LIMIT 1'); + var amount = 1; + for(row in result) { + Assert.isFalse(--amount < 0, "Invalid amount of rows in result"); + } + Assert.equals(0, amount); + } + + @Test + public function testStringIdRel() + { + var s = new ClassWithStringId(); + s.name = "first"; + s.field = 1; + s.insert(); + var v1 = new ClassWithStringIdRef(); + v1.ref = s; + v1.insert(); + var v2 = new ClassWithStringIdRef(); + v2.ref = s; + v2.insert(); + + s = new ClassWithStringId(); + s.name = "second"; + s.field = 2; + s.insert(); + v1 = new ClassWithStringIdRef(); + v1.ref = s; + v1.insert(); + s = null; v1 = null; v2 = null; + Manager.cleanup(); + + var first = ClassWithStringId.manager.search($name == "first"); + Assert.equals(1, first.length); + var first = first.first(); + Assert.equals(1, first.field); + var frel = ClassWithStringIdRef.manager.search($ref == first); + Assert.equals(2, frel.length); + for (rel in frel) + Assert.equals(first, rel.ref); + var frel2 = ClassWithStringIdRef.manager.search($ref_id == "first"); + Assert.equals(2, frel2.length); + for (rel in frel2) + Assert.equals(first, rel.ref); + + var second = ClassWithStringId.manager.search($name == "second"); + Assert.equals(1, second.length); + var second = second.first(); + Assert.equals(2, second.field); + var srel = ClassWithStringIdRef.manager.search($ref == second); + Assert.equals(1, srel.length); + for (rel in srel) + Assert.equals(second, rel.ref); + + Assert.equals(-1, frel.array().indexOf(srel.first())); + second.delete(); + for (r in srel) r.delete(); + first.delete(); + for (r in frel) r.delete(); + } + + @Test + public function testEnum() + { + var c1 = new OtherSpodClass("first spod"); + c1.insert(); + var c2 = new OtherSpodClass("second spod"); + c2.insert(); + + var scls = getDefaultClass(); + var scls1 = scls; + scls.relation = c1; + scls.insert(); + var id1 = scls.theId; + scls = getDefaultClass(); + scls.relation = c1; + scls.insert(); + + scls1.next = scls; + scls1.update(); + + var id2 = scls.theId; + scls = getDefaultClass(); + scls.relation = c1; + scls.next = scls1; + scls.anEnum = FirstValue; + scls.insert(); + var id3 = scls.theId; + scls = null; + + Manager.cleanup(); + var r1s = [ for (c in MySpodClass.manager.search($anEnum == SecondValue,{orderBy:theId})) c.theId ]; + Assert.deepEquals(r1s, [id1, id2]); + var r2s = MySpodClass.manager.search($anEnum == FirstValue); + Assert.equals(1, r2s.length); + Assert.equals(id3, r2s.first().theId); + Assert.equals(id1, r2s.first().next.theId); + Assert.equals(id2, r2s.first().next.next.theId); + + var fv = getSecond(); + var r1s = [ for (c in MySpodClass.manager.search($anEnum == fv,{orderBy:theId})) c.theId ]; + Assert.deepEquals(r1s, [id1, id2]); + var r2s = MySpodClass.manager.search($anEnum == getFirst()); + Assert.equals(1, r2s.length); + Assert.equals(id3, r2s.first().theId); + + var ids = [id1,id2,id3]; + var s = [ for (c in MySpodClass.manager.search( $anEnum == SecondValue || ($theId in ids) )) c.theId ]; + s.sort(Reflect.compare); + Assert.deepEquals(s, [id1, id2, id3]); + + r2s.first().delete(); + for (v in MySpodClass.manager.search($anEnum == fv)) v.delete(); + } + + public function getFirst() + { + return FirstValue; + } + + public function getSecond() + { + return SecondValue; + } + + @Test + public function testUpdate() + { + var c1 = new OtherSpodClass("first spod"); + c1.insert(); + var c2 = new OtherSpodClass("second spod"); + c2.insert(); + var scls = getDefaultClass(); + scls.relation = c1; + scls.relationNullable = c2; + scls.insert(); + + var id = scls.theId; + + //if no change made, update should return nothing + Assert.isNull(untyped MySpodClass.manager.getUpdateStatement(scls)); + Manager.cleanup(); + scls = MySpodClass.manager.get(id); + Assert.isNull(untyped MySpodClass.manager.getUpdateStatement(scls)); + scls.delete(); + + //try now with null SData and null relation + var scls = new NullableSpodClass(); + scls.insert(); + + var id = scls.theId; + + //if no change made, update should return nothing + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Manager.cleanup(); + scls = NullableSpodClass.manager.get(id); + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Assert.isNull(scls.data); + Assert.isNull(scls.relationNullable); + Assert.isNull(scls.abstractType); + Assert.isNull(scls.anEnum); + scls.delete(); + + //same thing with explicit null set + var scls = new NullableSpodClass(); + scls.data = null; + scls.relationNullable = null; + scls.abstractType = null; + scls.anEnum = null; + scls.insert(); + + var id = scls.theId; + + //if no change made, update should return nothing + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Manager.cleanup(); + scls = NullableSpodClass.manager.get(id); + Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + Assert.isNull(scls.data); + Assert.isNull(scls.relationNullable); + Assert.isNull(scls.abstractType); + Assert.isNull(scls.anEnum); + Manager.cleanup(); + + scls = new NullableSpodClass(); + scls.theId = id; + Assert.isNotNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); + + scls.delete(); + } + + @Test + public function testSpodTypes() + { + var c1 = new OtherSpodClass("first spod"); + c1.insert(); + var c2 = new OtherSpodClass("second spod"); + c2.insert(); + + var scls = getDefaultClass(); + + scls.relation = c1; + scls.relationNullable = c2; + scls.insert(); + + //after inserting, id must be filled + Assert.notEquals(0, scls.theId, pos()); + Assert.isNotNull(scls.theId); + var theid = scls.theId; + + c1 = c2 = null; + Manager.cleanup(); + + var cls1 = MySpodClass.manager.get(theid); + Assert.isNotNull(cls1, pos()); + //after Manager.cleanup(), the instances should be different + Assert.isFalse(cls1 == scls, pos()); + scls = null; + + Assert.isInstanceOf(cls1.int, Int, pos()); + Assert.equals(1, cls1.int, pos()); + Assert.isInstanceOf(cls1.double, Float, pos()); + Assert.equals(2.0, cls1.double, pos()); + Assert.isInstanceOf(cls1.boolean, Bool, pos()); + Assert.isTrue(cls1.boolean, pos()); + Assert.isInstanceOf(cls1.string, String, pos()); + Assert.equals("some string", cls1.string, pos()); + Assert.isInstanceOf(cls1.abstractType, String, pos()); + Assert.equals("other string", cls1.abstractType.get(), pos()); + Assert.isNotNull(cls1.date, pos()); + Assert.isInstanceOf(cls1.date, Date, pos()); + #if !php + // TODO : this fails with PHP7 + Assert.equals(new Date(2012, 7, 30, 0, 0, 0).getTime(), cls1.date.getTime(), pos()); + #end + + Assert.isInstanceOf(cls1.binary, Bytes, pos()); + Assert.equals(0, cls1.binary.compare(Bytes.ofString("\x01\n\r'\x02")), pos()); + Assert.isTrue(cls1.enumFlags.has(FirstValue), pos()); + Assert.isFalse(cls1.enumFlags.has(SecondValue), pos()); + Assert.isTrue(cls1.enumFlags.has(ThirdValue), pos()); + + Assert.isInstanceOf(cls1.data, Array, pos()); + Assert.isInstanceOf(cls1.data[0], ComplexClass, pos()); + + Assert.equals("test", cls1.data[0].val.name, pos()); + Assert.equals(4, cls1.data[0].val.array.length, pos()); + Assert.equals("is", cls1.data[0].val.array[1], pos()); + + Assert.equals("first spod", cls1.relation.name, pos()); + Assert.equals("second spod", cls1.relationNullable.name, pos()); + + Assert.equals(SecondValue, cls1.anEnum, pos()); + Assert.isInstanceOf(cls1.anEnum, SpodEnum, pos()); + + Assert.equals("\000a", cls1.bytes.toString()); + + Assert.equals(MySpodClass.manager.select($anEnum == SecondValue), cls1, pos()); + + //test create a new class + var scls = getDefaultClass(); + + c1 = new OtherSpodClass("third spod"); + c1.insert(); + + scls.relation = c1; + scls.insert(); + + scls = cls1 = null; + Manager.cleanup(); + + Assert.equals(2, MySpodClass.manager.all().length, pos()); + var req = MySpodClass.manager.search({ relation: OtherSpodClass.manager.select({ name:"third spod"} ) }); + Assert.equals(1, req.length, pos()); + scls = req.first(); + + scls.relation.name = "Test"; + scls.relation.update(); + + Assert.isNull(OtherSpodClass.manager.select({ name:"third spod" }), pos()); + + for (c in MySpodClass.manager.all()) + c.delete(); + for (c in OtherSpodClass.manager.all()) + c.delete(); + + //issue #3598 + var inexistent = MySpodClass.manager.get(1000,false); + Assert.isNull(inexistent); + } + + @Test + public function testDateQuery() + { + var other1 = new OtherSpodClass("required field"); + other1.insert(); + + var now = Date.now(); + var c1 = getDefaultClass(); + c1.relation = other1; + c1.date = now; + c1.insert(); + + var c2 = getDefaultClass(); + c2.relation = other1; + c2.date = DateTools.delta(now, DateTools.hours(1)); + c2.insert(); + + #if !php + // TODO : this fails with PHP7 + var q = MySpodClass.manager.search($date > now); + Assert.equals(1, q.length, pos()); + Assert.equals(c2, q.first(), pos()); + + q = MySpodClass.manager.search($date == now); + Assert.equals(1, q.length, pos()); + Assert.equals(c1, q.first(), pos()); + + q = MySpodClass.manager.search($date >= now); + Assert.equals(2, q.length, pos()); + Assert.equals(c1, q.first(), pos()); + + q = MySpodClass.manager.search($date >= DateTools.delta(now, DateTools.hours(2))); + Assert.equals(0, q.length, pos()); + Assert.isNull(q.first(), pos()); + #end + + c1.delete(); + c2.delete(); + other1.delete(); + } + + @Test + public function testData() + { + var other1 = new OtherSpodClass("required field"); + other1.insert(); + + var c1 = getDefaultClass(); + c1.relation = other1; + c1.insert(); + + Assert.equals(1, c1.data.length, pos()); + c1.data.pop(); + c1.update(); + + Manager.cleanup(); + c1 = null; + + c1 = MySpodClass.manager.select($relation == other1); + Assert.equals(0, c1.data.length, pos()); + c1.data.push(new ComplexClass({ name: "test1", array:["complex","field"] })); + c1.data.push(null); + Assert.equals(2, c1.data.length, pos()); + c1.update(); + + Manager.cleanup(); + c1 = null; + + c1 = MySpodClass.manager.select($relation == other1); + Assert.equals(2, c1.data.length, pos()); + Assert.equals("test1", c1.data[0].val.name, pos()); + Assert.equals(2, c1.data[0].val.array.length, pos()); + Assert.equals("complex", c1.data[0].val.array[0], pos()); + Assert.isNull(c1.data[1], pos()); + + c1.delete(); + other1.delete(); + } + + /** + Check that relations are not affected by the analyzer + + See: #6 and HaxeFoundation/haxe#6048 + + The way the analyzer transforms the expression (to prevent potential + side-effects) might change the context where `untyped __this__` is + evaluated. + **/ + @Test + public function testIssue6() + { + var parent = new MySpodClass(); + parent.relation = new OtherSpodClass("i"); + + Assert.isNotNull(parent.relation); + Assert.equals("i", parent.relation.name); + } + + private function pos(?p:haxe.PosInfos):haxe.PosInfos + { + p.fileName = p.fileName + "(" + Manager.cnx.dbName() +")"; + return p; + } +} diff --git a/testPHP.hxml b/testPHP.hxml new file mode 100644 index 0000000..6073e26 --- /dev/null +++ b/testPHP.hxml @@ -0,0 +1,8 @@ +-cp src +-cp test +-lib hexunit:0.35.0 +-lib jstack +-main Main +-D php7 +-D JSTACK_FORCE +-php ./