├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── extraParams.hxml ├── haxelib.json ├── src └── sys │ └── db │ ├── Manager.hx │ ├── Object.hx │ ├── RecordInfos.hx │ ├── RecordMacros.hx │ ├── TableCreate.hx │ ├── Transaction.hx │ └── Types.hx ├── test-common.hxml ├── test-cpp.hxml ├── test-hl.hxml ├── test-neko.hxml ├── test-php.hxml └── test ├── CommonDatabaseTest.hx ├── Main.hx ├── MySpodClass.hx ├── MysqlTest.hx └── SqliteTest.hx /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /build 3 | /test.sqlite 4 | /test.db 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: haxe 2 | os: linux 3 | dist: bionic 4 | 5 | haxe: 6 | - development 7 | - stable 8 | - 3.4.7 9 | 10 | before_install: 11 | #install php 7.2 on ubuntu trusty 12 | - sudo add-apt-repository ppa:ondrej/php -y 13 | - sudo apt-get update 14 | - sudo apt-get install -y php7.2 15 | - sudo apt-get install -y php7.2-mbstring 16 | - sudo apt-get install -y php7.2-sqlite3 17 | - sudo apt-get install -y php7.2-mysqli 18 | #create database in mysql5.6 19 | - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' 20 | # haxe libraries 21 | - haxelib install all --always 22 | # temporarily use hxcpp git (see #43) 23 | - haxelib git hxcpp https://github.com/HaxeFoundation/hxcpp.git --always 24 | - (cd "$(haxelib path hxcpp | head -n1)"/tools/hxcpp && haxe compile.hxml) 25 | 26 | services: 27 | - mysql 28 | 29 | install: 30 | - haxelib dev record-macros . 31 | 32 | script: 33 | - haxe test-neko.hxml 34 | - neko build/test.n mysql://travis:@127.0.0.1/test 35 | - php -v 36 | - haxe test-php.hxml 37 | - php build/php/index.php mysql://travis:@127.0.0.1/test 38 | - haxe test-cpp.hxml 39 | - build/cpp/Main-debug mysql://travis:@127.0.0.1/test 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Haxe Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/HaxeFoundation/record-macros.svg?branch=master)](https://travis-ci.org/HaxeFoundation/record-macros) 2 | 3 | Record macros is a macro-based library that provides object-relational mapping to Haxe. 4 | 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. 5 | 6 | ## Creating a Record 7 | You can simply declare a `record-macros` Object by extending the sys.db.Object class : 8 | 9 | ```haxe 10 | import sys.db.Types; 11 | 12 | class User extends sys.db.Object { 13 | public var id : SId; 14 | public var name : SString<32>; 15 | public var birthday : SDate; 16 | public var phoneNumber : SNull; 17 | } 18 | ``` 19 | 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 : 20 | 21 | * `Null, SNull` : tells that this field can be NULL in the database 22 | * `Int, SInt` : a classic 32 bits signed integer (SQL INT) 23 | * `Float, SFloat` : a double precision float value (SQL DOUBLE) 24 | * `Bool, SBool` : a boolean value (SQL TINYINT(1) or BOOL) 25 | * `Date, SDateTime` : a complete date value (SQL DATETIME) 26 | * `SDate` : a date-only value (SQL DATE) 27 | * `SString` : a size-limited string value (SQL VARCHAR(K)) 28 | * `String, SText` : a text up to 16 MB (SQL MEDIUMTEXT) 29 | * `SBytes` : a fixed-size bytes value (SQL BINARY(K)) 30 | * `SBinary, haxe.io.Bytes` : up to 16 MB bytes (SQL MEDIUMBLOB) 31 | * `SId` : same as SInt but used as an unique ID with auto increment (SQL INT AUTO INCREMENT) 32 | * `SEnum` : a single enum without parameters which index is stored as a small integer (SQL TINYINT UNSIGNED) 33 | * `SFlags` : a 32 bits flag that uses an enum as bit markers. See EnumFlags 34 | * `SData` : allow arbitrary serialized data (see below) 35 | 36 | ### Advanced Types 37 | 38 | The following advanced types are also available if you want a more custom storage size : 39 | 40 | * `SUInt` : an unsigned 32 bits integer (SQL UNSIGNED INT) 41 | * `STinyInt / STinyUInt` : a small 8 bits signed/unsigned integer (SQL TINYINT) 42 | * `SSmallInt / SSmallUInt` : a small 16 bits signed/unsigned integer (SQL SMALLINT) 43 | * `SMediumIInt / SMediumUInt` : a small 24 bits signed/unsigned integer (SQL MEDIUMINT) 44 | * `SBigInt` : a 64 bits signed integer (SQL BIGINT) - typed as Float in Haxe 45 | * `SSingle` : a single precision float value (SQL FLOAT) 46 | * `STinyText` : a text up to 255 bytes (SQL TINYTEXT) 47 | * `SSmallText` : a text up to 65KB (SQL TEXT) 48 | * `STimeStamp` : a 32-bits date timestamp (SQL TIMESTAMP) 49 | * `SSmallBinary` : up to 65 KB bytes (SQL BLOB) 50 | * `SLongBinary` : up to 4GB bytes (SQL LONGBLOB) 51 | * `SUId` : same as SUInt but used as an unique ID with auto increment (SQL INT UNSIGNED AUTO INCREMENT) 52 | * `SBigId` : same as SBigInt but used as an unique ID with auto increment (SQL BIGINT AUTO INCREMENT) - compiled as Float in Haxe 53 | * `SSmallFlags` : similar to SFlags except that the integer used to store the data is based on the number of flags allowed 54 | 55 | ## Metadata 56 | You can add Metadata to your `record-macros` class to declare additional informations that will be used by `record-macros`. 57 | 58 | Before each class field : 59 | 60 | * `@:skip` : ignore this field, which will not be part of the database schema 61 | * `@:relation` : declare this field as a relation (see specific section below) 62 | 63 | Before the `record-macros` class : 64 | 65 | * `@:table("myTableName")` : change the table name (by default it's the same as the class name) 66 | * `@: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 : 67 | 68 | ```haxe 69 | @:id(uid,gid) 70 | class UserGroup extends sys.db.Object { 71 | public var uid : SInt; 72 | public var gid : SInt; 73 | } 74 | ``` 75 | 76 | * `@: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) 77 | 78 | 79 | ## Init/Cleanup 80 | There are two static methods that you might need to call before/after using `record-macros` : 81 | 82 | * `sys.db.Manager.initialize()` : will initialize the created managers. Make sure to call it at least once before using `record-macros`. 83 | * `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. 84 | 85 | ## Creating the Table 86 | 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 : 87 | 88 | ```haxe 89 | var cnx = sys.db.Mysql.connect({ 90 | host : "localhost", 91 | port : null, 92 | user : "root", 93 | pass : "", 94 | database : "testBase", 95 | socket : null, 96 | }); 97 | sys.db.Manager.cnx = cnx; 98 | if ( !sys.db.TableCreate.exists(User.manager) ) 99 | { 100 | sys.db.TableCreate.create(User.manager); 101 | } 102 | ``` 103 | 104 | Please note that currently TableCreate will not create the index or initialize the relations of your table. 105 | 106 | ## Insert 107 | In order to insert a new `record-macros`, you can simply do the following : 108 | 109 | ```haxe 110 | var u = new User(); 111 | u.name = "Random156"; 112 | u.birthday = Date.now(); 113 | u.insert(); 114 | ``` 115 | 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) 116 | 117 | ## Manager 118 | 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 : 119 | 120 | ```haxe 121 | public static var manager = new sys.db.Manager(User); 122 | ``` 123 | 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. 124 | 125 | ## Get 126 | 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) : 127 | 128 | ```haxe 129 | var u = User.manager.get(1); 130 | if( u == null ) throw "User #1 not found"; 131 | trace(u.name); 132 | ``` 133 | If you have a primary key with multiple values, you can use the following declaration : 134 | 135 | ```haxe 136 | var ug = UserGroup.manager.get({ uid : 1, gid : 2 }); 137 | // ... 138 | ``` 139 | 140 | ## Update/Delete 141 | 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 : 142 | 143 | ```haxe 144 | var u = User.manager.get(1); 145 | if( u.phoneNumber == null ) u.phoneNumber = "+3360000000"; 146 | u.update(); 147 | ``` 148 | You can also use `.delete()` to delete this object from the database : 149 | 150 | ```haxe 151 | var u = User.manager.get(1); 152 | if( u != null ) u.delete(); 153 | ``` 154 | 155 | ## Search Queries 156 | If you want to search for some objects, you can use the `.manager.search` method : 157 | 158 | ```haxe 159 | var minId = 10; 160 | for( u in User.manager.search($id < minId) ) { 161 | trace(u); 162 | } 163 | ``` 164 | In order to differentiate between the database fields and the Haxe variables, all the database fields are prefixed with a dollar in search queries. 165 | 166 | Search queries are checked at compiletime and the following SQL code is generated instead : 167 | 168 | ```haxe 169 | unsafeSearch("SELECT * FROM User WHERE id < "+Manager.quoteInt(minId)); 170 | ``` 171 | The code generator also makes sure that no SQL injection is ever possible. 172 | 173 | ## Syntax 174 | The following syntax is supported : 175 | 176 | * constants : integers, floats, strings, null, true and false 177 | * all operations `+, -, *, /, %, |, &, ^, >>, <<, >>>` 178 | * unary operations `!, - and ~` 179 | * all comparisons : `== , >= , <=, >, <, !=` 180 | * bool tests : `&& , ||` 181 | * parenthesis 182 | * calls and fields accesses (compiled as Haxe expressions) 183 | 184 | 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. 185 | 186 | ## Additional Syntax 187 | It is also possible to use anonymous objects to match exact values for some fields (similar to previous `record-macros` but typed : 188 | 189 | ```haxe 190 | User.manager.search({ id : 1, name : "Nicolas" }) 191 | // same as : 192 | User.manager.search($id == 1 && $name == "Nicolas") 193 | // same as : 194 | User.manager.search($id == 1 && { name : "Nicolas" }) 195 | ``` 196 | 197 | You can also use if conditions to generate different SQL based on Haxe variables (you cannot use database fields in if test) : 198 | 199 | ```haxe 200 | function listName( ?name : String ) { 201 | return User.manager.search($id < 10 && if( name == null ) true else $name == name); 202 | } 203 | ``` 204 | 205 | ## SQL operations 206 | You can use the following SQL global functions in search queries : 207 | 208 | * `$now() : SDateTime`, returns the current datetime (SQL NOW()) 209 | * `$curDate() : SDate`, returns the current date (SQL CURDATE()) 210 | * `$date(v:SDateTime) : SDate`, returns the date part of the DateTime (SQL DATE()) 211 | * `$seconds(v:Float) : SInterval`, returns the date interval in seconds (SQL INTERVAL v SECOND) 212 | * `$minutes(v:Float) : SInterval`, returns the date interval in minutes (SQL INTERVAL v MINUTE) 213 | * `$hours(v:Float) : SInterval`, returns the date interval in hours (SQL INTERVAL v HOUR) 214 | * `$days(v:Float) : SInterval`, returns the date interval in days (SQL INTERVAL v DAY) 215 | * `$months(v:Float) : SInterval`, returns the date interval in months (SQL INTERVAL v MONTH) 216 | * `$years(v:Float) : SInterval`, returns the date interval in years (SQL INTERVAL v YEAR) 217 | 218 | You can use the following SQL operators in search queries : 219 | 220 | * `stringA.like(stringB)` : will use the SQL LIKE operator to find if stringB if contained into stringA 221 | 222 | ## SQL IN 223 | You can also use the Haxe in operator to get similar effect as SQL IN : 224 | 225 | ```haxe 226 | User.manager.search($name in ["a","b","c"]); 227 | ``` 228 | You can pass any Iterable to the in operator. An empty iterable will emit a false statement to prevent sql errors when doing IN (). 229 | 230 | ## Search Options 231 | After the search query, you can specify some search options : 232 | 233 | ```haxe 234 | // retrieve the first 20 users ordered by ascending name 235 | User.manager.search(true,{ orderBy : name, limit : 20 }); 236 | ``` 237 | 238 | The following options are supported : 239 | 240 | * `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 241 | * `limit` : specify which result range you want to obtain. You can use Haxe variables and expressions in limit values, for instance : { limit : [pos,length] } 242 | * `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 243 | 244 | ## Select/Count/Delete 245 | Instead of search you can use the `manager.select` method, which will only return the first result object : 246 | 247 | ```haxe 248 | var u = User.manager.select($name == "John"); 249 | // ... 250 | ``` 251 | You can also use the manager.count method to count the number of objects matching the given search query : 252 | 253 | ```haxe 254 | var n = User.manager.count($name.like("J%") && $phoneNumber != null); 255 | // ... 256 | ``` 257 | You can delete all objects matching the given query : 258 | 259 | ```haxe 260 | User.manager.delete($id > 1000); 261 | ``` 262 | 263 | ## Relations 264 | You can declare relations between your database classes by using the @:relation metadata : 265 | 266 | ```haxe 267 | class User extends sys.db.Object { 268 | public var id : SId; 269 | // .... 270 | } 271 | class Group extends sys.db.Object { 272 | public var id : SId; 273 | // ... 274 | } 275 | 276 | @:id(gid,uid) 277 | class UserGroup extends sys.db.Object { 278 | @:relation(uid) public var user : User; 279 | @:relation(gid) public var group : Group; 280 | } 281 | ``` 282 | 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. 283 | 284 | ## Locking 285 | 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 : 286 | 287 | ```haxe 288 | @:relation(uid,lock) public var user : User; 289 | ``` 290 | 291 | ## Cascading 292 | 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. 293 | 294 | However if the relation field can be nullable, the value will be set to NULL. 295 | 296 | If you want to enforce cascading for nullable-field relations, you can add the cascade keyword after the relation key : 297 | 298 | ```haxe 299 | @:relation(uid,cascade) var user : Null; 300 | ``` 301 | 302 | ## Relation Search 303 | You can search a given relation by using either the relation key or the relation property : 304 | 305 | ```haxe 306 | var user = User.manager.get(1); 307 | var groups = UserGroup.manager.search($uid == user.id); 308 | // same as : 309 | var groups = UserGroup.manager.search($user == user); 310 | ``` 311 | 312 | 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. 313 | 314 | ## Dynamic Search 315 | 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 : 316 | 317 | ```haxe 318 | var o = { name : "John", phoneNumber : "+818123456" }; 319 | var users = User.manager.dynamicSearch(o); 320 | ``` 321 | Please note that you can get runtime errors if your object contain fields that are not in the database table. 322 | 323 | ## Serialized Data 324 | 325 | In order to store arbitrary serialized data in a `record-macros` object, you can use the SData type. For example : 326 | 327 | ``` 328 | import sys.db.Types 329 | enum PhoneKind { 330 | AtHome; 331 | AtWork; 332 | Mobile; 333 | } 334 | class User extends sys.db.Object { 335 | public var id : SId; 336 | ... 337 | public var phones : SData>; 338 | } 339 | ``` 340 | 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 341 | When the phones field has been either read or written, a flag will be set to remember that potential changes were made 342 | 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 343 | As a consequence, pushing data into the phones Array or directly modifying the phone number will be noticed by the `record-macros` engine. 344 | 345 | 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. 346 | 347 | ## Accessing the record-macros Infos 348 | You can get the database schema by calling the `.dbInfos()` method on the Manager. It will return a `sys.db.RecordInfos` structure. 349 | 350 | ## Automatic Insert/Search/Edit Generation 351 | 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. 352 | 353 | ## Compatibility 354 | 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. 355 | 356 | ## Running the unit tests 357 | 358 | ``` 359 | # clone and checkout 360 | git clone https://github.com/HaxeFoundation/record-macros 361 | cd record-macros 362 | 363 | # prepare a test environment 364 | haxelib newrepo 365 | haxelib install all --always 366 | 367 | # install record-macros in dev mode 368 | # (necessary for extraParams.hxml to run) 369 | haxelib dev record-macros . 370 | 371 | # optional compiler flags: 372 | # -D UTEST_PATTERN= filter tests with pattern 373 | # -D UTEST_PRINT_TESTS print test names as they run 374 | # -D UTEST_FAILURE_THROW throw instead of report failures 375 | 376 | haxe test-.hxml 377 | mysql://:@[:]/ 378 | ``` 379 | -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | --macro addGlobalMetadata("Date.toString", "@:keep", false, false, true) 2 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "record-macros", 3 | "url": "https://github.com/HaxeFoundation/record-macros", 4 | "license": "MIT", 5 | "classPath": "src", 6 | "tags":["db","spod","orm","sql"], 7 | "description": "Macro-based ORM (object-relational mapping)", 8 | "version": "1.0.0-alpha", 9 | "releasenote": "Initial release", 10 | "contributors":["andyli","ncannasse","simn","waneck"] 11 | } 12 | -------------------------------------------------------------------------------- /src/sys/db/Manager.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2005-2016 Haxe Foundation 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | package sys.db; 23 | import Reflect; 24 | import sys.db.Connection; 25 | import sys.db.RecordInfos; 26 | 27 | /** 28 | Record Manager : the persistent object database manager. See the tutorial on 29 | Haxe website to learn how to use Record. 30 | **/ 31 | class Manager { 32 | 33 | /* ----------------------------- STATICS ------------------------------ */ 34 | public static var cnx(default, set) : Connection; 35 | public static var lockMode : String; 36 | 37 | private static inline var cache_field = "__cache__"; 38 | 39 | private static var object_cache : haxe.ds.StringMap = new haxe.ds.StringMap(); 40 | private static var init_list : List> = new List(); 41 | 42 | @:allow(sys.db.RecordMacros) 43 | private static var KEYWORDS = { 44 | var h = new haxe.ds.StringMap(); 45 | 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("|") ) 46 | h.set(k.toLowerCase(),true); 47 | h; 48 | } 49 | 50 | private static function set_cnx( c : Connection ) { 51 | cnx = c; 52 | switch( cnx ) { 53 | case null: 54 | case _.dbName() => "MySQL": 55 | lockMode = " FOR UPDATE"; 56 | cnx.request("SET sql_mode = 'PIPES_AS_CONCAT'"); 57 | default: 58 | lockMode = ""; 59 | } 60 | return cnx; 61 | } 62 | 63 | /* ---------------------------- BASIC API ----------------------------- */ 64 | 65 | var table_infos : RecordInfos; 66 | var table_name : String; 67 | var table_keys : Array; 68 | var class_proto : { prototype : Dynamic }; 69 | 70 | public function new( classval : Class ) { 71 | var m : Array = haxe.rtti.Meta.getType(classval).rtti; 72 | if( m == null ) throw "Missing @rtti for class " + Type.getClassName(classval); 73 | table_infos = haxe.Unserializer.run(m[0]); 74 | table_name = quoteField(table_infos.name); 75 | table_keys = table_infos.key; 76 | // set the manager and ready for further init 77 | class_proto = cast classval; 78 | #if neko 79 | class_proto.prototype._manager = this; 80 | init_list.add(this); 81 | #end 82 | } 83 | 84 | public function all( ?lock: Bool ) : List { 85 | return unsafeObjects("SELECT * FROM " + table_name,lock); 86 | } 87 | 88 | public macro function get(ethis,id,?lock:haxe.macro.Expr.ExprOf) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { 89 | return RecordMacros.macroGet(ethis,id,lock); 90 | } 91 | 92 | public macro function select(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprOf) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { 93 | return RecordMacros.macroSearch(ethis, cond, options, lock, true); 94 | } 95 | 96 | public macro function search(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprOf) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf> #end { 97 | return RecordMacros.macroSearch(ethis, cond, options, lock); 98 | } 99 | 100 | public macro function count(ethis, cond) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { 101 | return RecordMacros.macroCount(ethis, cond); 102 | } 103 | 104 | public macro function delete(ethis, cond, ?options) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprOf #end { 105 | return RecordMacros.macroDelete(ethis, cond, options); 106 | } 107 | 108 | public function dynamicSearch( x : {}, ?lock : Bool ) : List { 109 | var s = new StringBuf(); 110 | s.add("SELECT * FROM "); 111 | s.add(table_name); 112 | s.add(" WHERE "); 113 | addCondition(s,x); 114 | return unsafeObjects(s.toString(),lock); 115 | } 116 | 117 | function quote( s : String ) : String { 118 | return getCnx().quote( s ); 119 | } 120 | 121 | /* -------------------------- RECORDOBJECT API -------------------------- */ 122 | 123 | function doUpdateCache( x : T, name : String, v : Dynamic ) { 124 | var cache : { v : Dynamic } = Reflect.field(x, "cache_" + name); 125 | // if the cache has not been fetched (for instance if the field was set by reflection) 126 | // then we directly use the new value 127 | if( cache == null ) 128 | return v; 129 | var v = doSerialize(name, cache.v); 130 | // don't set it since the value might change again later 131 | // Reflect.setField(x, name, v); 132 | return v; 133 | } 134 | 135 | static function getFieldName(field:RecordField):String 136 | { 137 | return switch (field.t) { 138 | case DData | DEnum(_): 139 | "data_" + field.name; 140 | case _: 141 | field.name; 142 | } 143 | } 144 | 145 | function doInsert( x : T ) { 146 | unmake(x); 147 | var s = new StringBuf(); 148 | var fields = new List(); 149 | var values = new List(); 150 | var cache = Reflect.field(x,cache_field); 151 | if (cache == null) 152 | { 153 | Reflect.setField(x,cache_field,cache = {}); 154 | } 155 | 156 | for( f in table_infos.fields ) { 157 | var name = f.name, 158 | fieldName = getFieldName(f); 159 | var v:Dynamic = Reflect.field(x,fieldName); 160 | if( v != null ) { 161 | fields.add(quoteField(name)); 162 | switch( f.t ) { 163 | case DData: v = doUpdateCache(x, name, v); 164 | default: 165 | } 166 | values.add(v); 167 | } else if( !f.isNull ) { 168 | // if the field is not defined, give it a default value on insert 169 | switch( f.t ) { 170 | case DUInt, DTinyInt, DInt, DSingle, DFloat, DFlags(_), DBigInt, DTinyUInt, DSmallInt, DSmallUInt, DMediumInt, DMediumUInt, DEnum(_): 171 | Reflect.setField(x, fieldName, 0); 172 | case DBool: 173 | Reflect.setField(x, fieldName, false); 174 | case DTinyText, DText, DString(_), DSmallText, DSerialized: 175 | Reflect.setField(x, fieldName, ""); 176 | case DSmallBinary, DNekoSerialized, DLongBinary, DBytes(_), DBinary: 177 | Reflect.setField(x, fieldName, haxe.io.Bytes.alloc(0)); 178 | case DDate, DDateTime, DTimeStamp: 179 | // default date might depend on database 180 | case DId, DUId, DBigId, DNull, DInterval, DEncoded, DData: 181 | // no default value for these 182 | } 183 | } 184 | 185 | Reflect.setField(cache, name, v); 186 | } 187 | s.add("INSERT INTO "); 188 | s.add(table_name); 189 | if (fields.length > 0 || getCnx().dbName() != "SQLite") 190 | { 191 | s.add(" ("); 192 | s.add(fields.join(",")); 193 | s.add(") VALUES ("); 194 | var first = true; 195 | for( v in values ) { 196 | if( first ) 197 | first = false; 198 | else 199 | s.add(", "); 200 | getCnx().addValue(s,v); 201 | } 202 | s.add(")"); 203 | } else { 204 | s.add(" DEFAULT VALUES"); 205 | } 206 | unsafeExecute(s.toString()); 207 | untyped x._lock = true; 208 | // table with one key not defined : suppose autoincrement 209 | if( table_keys.length == 1 && Reflect.field(x,table_keys[0]) == null ) 210 | Reflect.setField(x,table_keys[0],getCnx().lastInsertId()); 211 | addToCache(x); 212 | } 213 | 214 | inline function isBinary( t : RecordInfos.RecordType ) { 215 | return switch( t ) { 216 | case DSmallBinary, DNekoSerialized, DLongBinary, DBytes(_), DBinary: true; 217 | //case DData: true // -- disabled for implementation purposes 218 | default: false; 219 | }; 220 | } 221 | 222 | inline function hasBinaryChanged( a : haxe.io.Bytes, b : haxe.io.Bytes ) { 223 | return a != b && (a == null || b == null || a.compare(b) != 0); 224 | } 225 | 226 | function doUpdate( x : T ) { 227 | if( untyped !x._lock ) 228 | throw "Cannot update a not locked object"; 229 | var upd = getUpdateStatement(x); 230 | if (upd == null) return; 231 | unsafeExecute(upd); 232 | } 233 | 234 | function getUpdateStatement( x : T ):Null 235 | { 236 | unmake(x); 237 | var s = new StringBuf(); 238 | s.add("UPDATE "); 239 | s.add(table_name); 240 | s.add(" SET "); 241 | var cache = Reflect.field(x,cache_field); 242 | var mod = false; 243 | for( f in table_infos.fields ) { 244 | if (table_keys.indexOf(f.name) >= 0) 245 | continue; 246 | var name = f.name, 247 | fieldName = getFieldName(f); 248 | var v : Dynamic = Reflect.field(x,fieldName); 249 | var vc : Dynamic = Reflect.field(cache,name); 250 | if( cache == null || v != vc ) { 251 | switch( f.t ) { 252 | case DSmallBinary, DNekoSerialized, DLongBinary, DBytes(_), DBinary: 253 | if ( !hasBinaryChanged(v,vc) ) 254 | continue; 255 | case DData: 256 | v = doUpdateCache(x, name, v); 257 | if( !hasBinaryChanged(v,vc) ) 258 | continue; 259 | default: 260 | } 261 | if( mod ) 262 | s.add(", "); 263 | else 264 | mod = true; 265 | s.add(quoteField(name)); 266 | s.add(" = "); 267 | getCnx().addValue(s,v); 268 | if ( cache != null ) 269 | Reflect.setField(cache,name,v); 270 | } 271 | } 272 | if( !mod ) 273 | return null; 274 | s.add(" WHERE "); 275 | addKeys(s,x); 276 | return s.toString(); 277 | } 278 | 279 | function doDelete( x : T ) { 280 | var s = new StringBuf(); 281 | s.add("DELETE FROM "); 282 | s.add(table_name); 283 | s.add(" WHERE "); 284 | addKeys(s,x); 285 | unsafeExecute(s.toString()); 286 | removeFromCache(x); 287 | } 288 | 289 | function doLock( i : T ) { 290 | if( untyped i._lock ) 291 | return; 292 | var s = new StringBuf(); 293 | s.add("SELECT * FROM "); 294 | s.add(table_name); 295 | s.add(" WHERE "); 296 | addKeys(s, i); 297 | // will force sync 298 | if( unsafeObject(s.toString(),true) != i ) 299 | throw "Could not lock object (was deleted ?); try restarting transaction"; 300 | } 301 | 302 | function objectToString( it : T ) : String { 303 | var s = new StringBuf(); 304 | s.add(table_name); 305 | if( table_keys.length == 1 ) { 306 | s.add("#"); 307 | s.add(Reflect.field(it,table_keys[0])); 308 | } else { 309 | s.add("("); 310 | var first = true; 311 | for( f in table_keys ) { 312 | if( first ) 313 | first = false; 314 | else 315 | s.add(","); 316 | s.add(quoteField(f)); 317 | s.add(":"); 318 | s.add(Reflect.field(it,f)); 319 | } 320 | s.add(")"); 321 | } 322 | return s.toString(); 323 | } 324 | 325 | function doSerialize( field : String, v : Dynamic ) : haxe.io.Bytes { 326 | var s = new haxe.Serializer(); 327 | s.useEnumIndex = true; 328 | s.serialize(v); 329 | var str = s.toString(); 330 | #if neko 331 | return neko.Lib.bytesReference(str); 332 | #else 333 | return haxe.io.Bytes.ofString(str); 334 | #end 335 | } 336 | 337 | function doUnserialize( field : String, b : haxe.io.Bytes ) : Dynamic { 338 | if( b == null ) 339 | return null; 340 | var str; 341 | #if neko 342 | str = neko.Lib.stringReference(b); 343 | #else 344 | str = b.toString(); 345 | #end 346 | if( str == "" ) 347 | return null; 348 | return haxe.Unserializer.run(str); 349 | } 350 | 351 | /* ---------------------------- INTERNAL API -------------------------- */ 352 | 353 | function normalizeCache(x:CacheType) 354 | { 355 | for (f in Reflect.fields(x) ) 356 | { 357 | var val:Dynamic = Reflect.field(x,f), info = table_infos.hfields.get(f); 358 | if (info != null) 359 | { 360 | if (val != null) switch (info.t) { 361 | case DDate, DDateTime if (!Std.is(val,Date)): 362 | if (Std.is(val,Float)) 363 | { 364 | val = Date.fromTime(val); 365 | } else { 366 | var v = val + ""; 367 | var index = v.indexOf('.'); 368 | if (index >= 0) 369 | v = v.substr(0,index); 370 | val = Date.fromString(v); 371 | } 372 | case DSmallBinary, DLongBinary, DBinary, DBytes(_), DData: 373 | if (Std.is(val, String)) 374 | val = haxe.io.Bytes.ofString(val); 375 | #if cpp 376 | else if (Std.is(val, haxe.io.BytesData)) 377 | val = haxe.io.Bytes.ofData(val); 378 | #end 379 | case DString(_) | DTinyText | DSmallText | DText if(!Std.is(val,String)): 380 | val = val + ""; 381 | #if (cs && erase_generics) 382 | // on C#, SQLite Ints are returned as Int64 383 | case DInt if (!Std.is(val,Int)): 384 | val = cast(val,Int); 385 | #end 386 | case DBool if (!Std.is(val,Bool)): 387 | if (Std.is(val,Int)) 388 | val = val != 0; 389 | else if (Std.is(val, String)) switch (val.toLowerCase()) { 390 | case "1", "true": val = true; 391 | case "0", "false": val = false; 392 | } 393 | case DFloat if (Std.is(val,String)): 394 | val = Std.parseFloat(val); 395 | case _: 396 | } 397 | Reflect.setField(x, f, val); 398 | } 399 | } 400 | } 401 | 402 | function cacheObject( x : CacheType, lock : Bool ) { 403 | #if neko 404 | var o = untyped __dollar__new(x); 405 | untyped __dollar__objsetproto(o, class_proto.prototype); 406 | #else 407 | var o : T = Type.createEmptyInstance(cast class_proto); 408 | untyped o._manager = this; 409 | #end 410 | normalizeCache(x); 411 | for (f in Reflect.fields(x) ) 412 | { 413 | var val:Dynamic = Reflect.field(x,f), info = table_infos.hfields.get(f); 414 | if (info != null) 415 | { 416 | var fieldName = getFieldName(info); 417 | Reflect.setField(o, fieldName, val); 418 | } 419 | } 420 | Reflect.setField(o,cache_field,x); 421 | addToCache(o); 422 | untyped o._lock = lock; 423 | return o; 424 | } 425 | 426 | function make( x : T ) { 427 | } 428 | 429 | function unmake( x : T ) { 430 | } 431 | 432 | function quoteField(f : String) { 433 | return KEYWORDS.exists(f.toLowerCase()) ? "`"+f+"`" : f; 434 | } 435 | 436 | function addKeys( s : StringBuf, x : {} ) { 437 | var first = true; 438 | for( k in table_keys ) { 439 | if( first ) 440 | first = false; 441 | else 442 | s.add(" AND "); 443 | s.add(quoteField(k)); 444 | s.add(" = "); 445 | var f = Reflect.field(x,k); 446 | if( f == null ) 447 | throw ("Missing key "+k); 448 | getCnx().addValue(s,f); 449 | } 450 | } 451 | 452 | function unsafeExecute( sql : String ) { 453 | return getCnx().request(sql); 454 | } 455 | 456 | public function unsafeObject( sql : String, lock : Bool ) : T { 457 | if( lock != false ) { 458 | lock = true; 459 | sql += getLockMode(); 460 | } 461 | var r = unsafeExecute(sql); 462 | var r = r.hasNext() ? r.next() : null; 463 | if( r == null ) 464 | return null; 465 | normalizeCache(r); 466 | var c = getFromCache(r,lock); 467 | if( c != null ) 468 | return c; 469 | r = cacheObject(r,lock); 470 | make(r); 471 | return r; 472 | } 473 | 474 | public function unsafeObjects( sql : String, lock : Bool ) : List { 475 | if( lock != false ) { 476 | lock = true; 477 | sql += getLockMode(); 478 | } 479 | var l = unsafeExecute(sql).results(); 480 | var l2 = new List(); 481 | for( x in l ) { 482 | normalizeCache(x); 483 | var c = getFromCache(x,lock); 484 | if( c != null ) 485 | l2.add(c); 486 | else { 487 | x = cacheObject(x,lock); 488 | make(x); 489 | l2.add(x); 490 | } 491 | } 492 | return l2; 493 | } 494 | 495 | public function unsafeCount( sql : String ) { 496 | return unsafeExecute(sql).getIntResult(0); 497 | } 498 | 499 | public function unsafeDelete( sql : String ) { 500 | unsafeExecute(sql); 501 | } 502 | 503 | public function unsafeGet( id : Dynamic, ?lock : Bool ) : T { 504 | if( lock == null ) lock = true; 505 | if( table_keys.length != 1 ) 506 | throw "Invalid number of keys"; 507 | if( id == null ) 508 | return null; 509 | var x : Dynamic = getFromCacheKey(Std.string(id) + table_name); 510 | if( x != null && (!lock || x._lock) ) 511 | return x; 512 | var s = new StringBuf(); 513 | s.add("SELECT * FROM "); 514 | s.add(table_name); 515 | s.add(" WHERE "); 516 | s.add(quoteField(table_keys[0])); 517 | s.add(" = "); 518 | getCnx().addValue(s,id); 519 | return unsafeObject(s.toString(), lock); 520 | } 521 | 522 | public function unsafeGetWithKeys( keys : { }, ?lock : Bool ) : T { 523 | if( lock == null ) lock = true; 524 | var x : Dynamic = getFromCacheKey(makeCacheKey(cast keys)); 525 | if( x != null && (!lock || x._lock) ) 526 | return x; 527 | var s = new StringBuf(); 528 | s.add("SELECT * FROM "); 529 | s.add(table_name); 530 | s.add(" WHERE "); 531 | addKeys(s,keys); 532 | return unsafeObject(s.toString(),lock); 533 | } 534 | 535 | public function unsafeGetId( o : T ) : Dynamic { 536 | return o == null ? null : Reflect.field(o, table_keys[0]); 537 | } 538 | 539 | public static function nullCompare( a : String, b : String, eq : Bool ) { 540 | if (a == null || a == 'NULL') { 541 | return eq ? '$b IS NULL' : '$b IS NOT NULL'; 542 | } else if (b == null || b == 'NULL') { 543 | return eq ? '$a IS NULL' : '$a IS NOT NULL'; 544 | } 545 | // we can't use a null-safe operator here 546 | if( cnx.dbName() != "MySQL" ) 547 | return a + (eq ? " = " : " != ") + b; 548 | var sql = a+" <=> "+b; 549 | if( !eq ) sql = "NOT("+sql+")"; 550 | return sql; 551 | } 552 | 553 | function addCondition(s : StringBuf,x) { 554 | var first = true; 555 | if( x != null ) 556 | for( f in Reflect.fields(x) ) { 557 | if( first ) 558 | first = false; 559 | else 560 | s.add(" AND "); 561 | s.add(quoteField(f)); 562 | var d = Reflect.field(x,f); 563 | if( d == null ) 564 | s.add(" IS NULL"); 565 | else { 566 | s.add(" = "); 567 | getCnx().addValue(s,d); 568 | } 569 | } 570 | if( first ) 571 | getCnx().addValue(s, true); 572 | } 573 | 574 | /* --------------------------- MISC API ------------------------------ */ 575 | 576 | public function dbClass() : Class { 577 | return cast class_proto; 578 | } 579 | 580 | public function dbInfos() { 581 | return table_infos; 582 | } 583 | 584 | function getCnx() { 585 | return cnx; 586 | } 587 | 588 | function getLockMode() { 589 | return lockMode; 590 | } 591 | 592 | /** 593 | Remove the cached value for the given Object field : this will ensure 594 | that the value is updated when calling .update(). This is necessary if 595 | you are modifying binary data in-place since the cache will be modified 596 | as well. 597 | **/ 598 | public function forceUpdate( o : T, field : String ) { 599 | // set a reference that will ensure != and .compare() != 0 600 | Reflect.setField(Reflect.field(o,cache_field),field,null); 601 | } 602 | 603 | /* --------------------------- INIT / CLEANUP ------------------------- */ 604 | 605 | public static function initialize() { 606 | var l = init_list; 607 | init_list = new List(); 608 | for( m in l ) 609 | for( r in m.table_infos.relations ) 610 | m.initRelation(r); 611 | } 612 | 613 | public static function cleanup() { 614 | object_cache = new haxe.ds.StringMap(); 615 | } 616 | 617 | function initRelation( r : RecordInfos.RecordRelation ) { 618 | // setup getter/setter 619 | var spod : Dynamic = Type.resolveClass(r.type); 620 | if( spod == null ) throw "Missing spod type " + r.type; 621 | var manager : Manager = spod.manager; 622 | var hprop = "__"+r.prop; 623 | var hkey = r.key; 624 | var lock = r.lock; 625 | if( manager == null || manager.table_keys == null ) throw ("Invalid manager for relation "+table_name+":"+r.prop); 626 | if( manager.table_keys.length != 1 ) throw ("Relation " + r.prop + "(" + r.key + ") on a multiple key table"); 627 | } 628 | 629 | function __get( x : Dynamic, prop : String, key : String, lock ) { 630 | var v = Reflect.field(x,prop); 631 | if( v != null ) 632 | return v; 633 | v = unsafeGet(Reflect.field(x, key), lock); 634 | Reflect.setField(x, prop, v); 635 | return v; 636 | } 637 | 638 | function __set( x : Dynamic, prop : String, key : String, v : T ) { 639 | Reflect.setField(x,prop,v); 640 | if( v == null ) 641 | Reflect.setField(x,key,null); 642 | else 643 | Reflect.setField(x,key,Reflect.field(v,table_keys[0])); 644 | return v; 645 | } 646 | 647 | /* ---------------------------- OBJECT CACHE -------------------------- */ 648 | 649 | function makeCacheKey( x : CacheType ) : String { 650 | if( table_keys.length == 1 ) { 651 | var k = Reflect.field(x,table_keys[0]); 652 | if( k == null ) 653 | throw("Missing key "+table_keys[0]); 654 | return Std.string(k)+table_name; 655 | } 656 | var s = new StringBuf(); 657 | for( k in table_keys ) { 658 | var v = Reflect.field(x,k); 659 | if( k == null ) 660 | throw("Missing key "+k); 661 | s.add(v); 662 | s.add("#"); 663 | } 664 | s.add(table_name); 665 | return s.toString(); 666 | } 667 | 668 | function addToCache( x : CacheType ) { 669 | object_cache.set(makeCacheKey(x),x); 670 | } 671 | 672 | function removeFromCache( x : CacheType ) { 673 | object_cache.remove(makeCacheKey(x)); 674 | } 675 | 676 | function getFromCacheKey( key : String ) : T { 677 | return cast object_cache.get(key); 678 | } 679 | 680 | function getFromCache( x : CacheType, lock : Bool ) : T { 681 | var c : Dynamic = object_cache.get(makeCacheKey(x)); 682 | if( c != null && lock && !c._lock ) { 683 | // synchronize the fields since our result is up-to-date ! 684 | for( f in Reflect.fields(c) ) { 685 | try { 686 | Reflect.setField(c,f,null); 687 | } catch (_:Dynamic) { 688 | // hack: ignore non-physical fields on hxcpp 689 | } 690 | } 691 | for (f in table_infos.fields) 692 | { 693 | var name = f.name, 694 | fieldName = getFieldName(f); 695 | Reflect.setField(c,fieldName,Reflect.field(x,name)); 696 | } 697 | // mark as locked 698 | c._lock = true; 699 | // restore our manager 700 | #if !neko 701 | untyped c._manager = this; 702 | #end 703 | // use the new object as our cache of fields 704 | Reflect.setField(c,cache_field,x); 705 | // remake object 706 | make(c); 707 | } 708 | return c; 709 | } 710 | 711 | /* ---------------------------- QUOTES -------------------------- */ 712 | 713 | public static function quoteAny( v : Dynamic ) { 714 | if (v == null) { 715 | return 'NULL'; 716 | } 717 | 718 | var s = new StringBuf(); 719 | cnx.addValue(s, v); 720 | return s.toString(); 721 | } 722 | 723 | public static function quoteList( v : String, it : Iterable ) { 724 | var b = new StringBuf(); 725 | var first = true; 726 | if( it != null ) 727 | for( v in it ) { 728 | if( first ) first = false else b.addChar(','.code); 729 | cnx.addValue(b, v); 730 | } 731 | if( first ) 732 | return quoteAny(false); 733 | return v + " IN (" + b.toString() + ")"; 734 | } 735 | 736 | // We need Bytes.toString to not be DCE'd. See #1937 737 | @:keep static function __depends() { return haxe.io.Bytes.alloc(0).toString(); } 738 | } 739 | 740 | private typedef CacheType = Dynamic; 741 | -------------------------------------------------------------------------------- /src/sys/db/Object.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2005-2016 Haxe Foundation 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | package sys.db; 23 | 24 | /** 25 | Record Object : the persistent object base type. See the tutorial on Haxe 26 | website to learn how to use Record. 27 | **/ 28 | @:keepSub 29 | @:autoBuild(sys.db.RecordMacros.macroBuild()) @:skipFields 30 | #if ((haxe_ver < 4.0) && php) @:native("sys.db.Object_hx") #end 31 | class Object { 32 | 33 | var _lock(default,never) : Bool; 34 | var _manager(default,never) : sys.db.Manager; 35 | #if !neko 36 | @:keep var __cache__:Dynamic; 37 | #end 38 | 39 | public function new() { 40 | #if !neko 41 | if( _manager == null ) untyped _manager = __getManager(); 42 | #end 43 | } 44 | 45 | #if !neko 46 | private function __getManager():sys.db.Manager 47 | { 48 | var cls:Dynamic = Type.getClass(this); 49 | return cls.manager; 50 | } 51 | #end 52 | 53 | public function insert() { 54 | untyped _manager.doInsert(this); 55 | } 56 | 57 | public function update() { 58 | untyped _manager.doUpdate(this); 59 | } 60 | 61 | public function lock() { 62 | untyped _manager.doLock(this); 63 | } 64 | 65 | public function delete() { 66 | untyped _manager.doDelete(this); 67 | } 68 | 69 | public function isLocked() { 70 | return _lock; 71 | } 72 | 73 | public function toString() : String { 74 | return untyped _manager.objectToString(this); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/sys/db/RecordInfos.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2005-2016 Haxe Foundation 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | package sys.db; 23 | 24 | enum RecordType { 25 | DId; 26 | DInt; 27 | DUId; 28 | DUInt; 29 | DBigId; 30 | DBigInt; 31 | DSingle; 32 | DFloat; 33 | DBool; 34 | DString( n : Int ); 35 | DDate; 36 | DDateTime; 37 | DTimeStamp; 38 | DTinyText; 39 | DSmallText; 40 | DText; 41 | DSmallBinary; 42 | DLongBinary; 43 | DBinary; 44 | DBytes( n : Int ); 45 | DEncoded; 46 | DSerialized; 47 | DNekoSerialized; 48 | DFlags( flags : Array, autoSize : Bool ); 49 | DTinyInt; 50 | DTinyUInt; 51 | DSmallInt; 52 | DSmallUInt; 53 | DMediumInt; 54 | DMediumUInt; 55 | DData; 56 | DEnum( name : String ); 57 | // specific for intermediate calculus 58 | DInterval; 59 | DNull; 60 | } 61 | 62 | typedef RecordField = { 63 | var name : String; 64 | var t : RecordType; 65 | var isNull : Bool; 66 | } 67 | 68 | typedef RecordRelation = { 69 | var prop : String; 70 | var key : String; 71 | var type : String; 72 | var module : String; 73 | var cascade : Bool; 74 | var lock : Bool; 75 | var isNull : Bool; 76 | } 77 | 78 | typedef RecordInfos = { 79 | var name : String; 80 | var key : Array; 81 | var fields : Array; 82 | var hfields : Map; 83 | var relations : Array; 84 | var indexes : Array<{ keys : Array, unique : Bool }>; 85 | } 86 | -------------------------------------------------------------------------------- /src/sys/db/RecordMacros.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2005-2016 Haxe Foundation 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | package sys.db; 23 | import sys.db.RecordInfos; 24 | import haxe.macro.Expr; 25 | import haxe.macro.Type.VarAccess; 26 | #if macro 27 | import haxe.macro.Context; 28 | using haxe.macro.TypeTools; 29 | #end 30 | using Lambda; 31 | 32 | private typedef SqlFunction = { 33 | var name : String; 34 | var params : Array; 35 | var ret : RecordType; 36 | var sql : String; 37 | } 38 | 39 | class RecordMacros { 40 | 41 | static var GLOBAL = null; 42 | static var simpleString = ~/^[A-Za-z0-9 ]*$/; 43 | 44 | var isNull : Bool; 45 | var manager : Expr; 46 | var inf : RecordInfos; 47 | var g : { 48 | var cache : haxe.ds.StringMap; 49 | var types : haxe.ds.StringMap; 50 | var functions : haxe.ds.StringMap; 51 | }; 52 | 53 | function new(c) { 54 | if( GLOBAL != null ) 55 | g = GLOBAL; 56 | else { 57 | g = initGlobals(); 58 | GLOBAL = g; 59 | } 60 | inf = getRecordInfos(c); 61 | } 62 | 63 | function initGlobals() 64 | { 65 | var cache = new haxe.ds.StringMap(); 66 | var types = new haxe.ds.StringMap(); 67 | for( c in Type.getEnumConstructs(RecordType) ) { 68 | var e : Dynamic = Reflect.field(RecordType, c); 69 | if( Std.is(e, RecordType) ) 70 | types.set("S"+c.substr(1), e); 71 | } 72 | types.remove("SNull"); 73 | var functions = new haxe.ds.StringMap(); 74 | for( f in [ 75 | { name : "now", params : [], ret : DDateTime, sql : "NOW($)" }, 76 | { name : "curDate", params : [], ret : DDate, sql : "CURDATE($)" }, 77 | { name : "seconds", params : [DFloat], ret : DInterval, sql : "INTERVAL $ SECOND" }, 78 | { name : "minutes", params : [DFloat], ret : DInterval, sql : "INTERVAL $ MINUTE" }, 79 | { name : "hours", params : [DFloat], ret : DInterval, sql : "INTERVAL $ HOUR" }, 80 | { name : "days", params : [DFloat], ret : DInterval, sql : "INTERVAL $ DAY" }, 81 | { name : "months", params : [DFloat], ret : DInterval, sql : "INTERVAL $ MONTH" }, 82 | { name : "years", params : [DFloat], ret : DInterval, sql : "INTERVAL $ YEAR" }, 83 | { name : "date", params : [DDateTime], ret : DDate, sql : "DATE($)" }, 84 | ]) 85 | functions.set(f.name, f); 86 | return { cache : cache, types : types, functions : functions }; 87 | } 88 | 89 | public dynamic function error( msg : String, pos : Position ) : Dynamic { 90 | #if macro 91 | Context.error(msg, pos); 92 | #else 93 | throw msg; 94 | #end 95 | return null; 96 | } 97 | 98 | public dynamic function typeof( e : Expr ) : haxe.macro.Type { 99 | #if macro 100 | return Context.typeof(e); 101 | #else 102 | throw "not implemented"; 103 | return null; 104 | #end 105 | } 106 | 107 | public dynamic function follow( t : haxe.macro.Type, ?once ) : haxe.macro.Type { 108 | #if macro 109 | return Context.follow(t,once); 110 | #else 111 | throw "not implemented"; 112 | return null; 113 | #end 114 | } 115 | 116 | public dynamic function getManager( t : haxe.macro.Type, p : Position ) : RecordMacros { 117 | #if macro 118 | return getManagerInfos(t, p); 119 | #else 120 | throw "not implemented"; 121 | return null; 122 | #end 123 | } 124 | 125 | public dynamic function resolveType( name : String, ?module : String ) : haxe.macro.Type { 126 | #if macro 127 | if (module != null) 128 | { 129 | var m = Context.getModule(module); 130 | for (t in m) 131 | { 132 | if (t.toString() == name) 133 | return t; 134 | } 135 | } 136 | 137 | return Context.getType(name); 138 | #else 139 | throw "not implemented"; 140 | return null; 141 | #end 142 | } 143 | 144 | function makeInt( t : haxe.macro.Type ) { 145 | switch( t ) { 146 | case TInst(c, _): 147 | var name = c.toString(); 148 | if( name.charAt(0) == "I" ) 149 | return Std.parseInt(name.substr(1)); 150 | default: 151 | } 152 | throw "Unsupported " + Std.string(t); 153 | } 154 | 155 | function makeRecord( t : haxe.macro.Type ) { 156 | switch( t ) { 157 | case TInst(c, _): 158 | var name = c.toString(); 159 | var cl = c.get(); 160 | var csup = cl.superClass; 161 | while( csup != null ) { 162 | if( csup.t.toString() == "sys.db.Object" ) 163 | return c; 164 | csup = csup.t.get().superClass; 165 | } 166 | case TType(t, p) | TAbstract(t, p): 167 | var name = t.toString(); 168 | if( p.length == 1 && (name == "Null" || name == "sys.db.SNull") ) { 169 | isNull = true; 170 | return makeRecord(p[0]); 171 | } 172 | default: 173 | } 174 | return null; 175 | } 176 | 177 | function getFlags( t : haxe.macro.Type ) { 178 | switch( t ) { 179 | case TEnum(e,_): 180 | var cl = e.get().names; 181 | if( cl.length > 1 ) { 182 | var prefix = cl[0]; 183 | for( c in cl ) 184 | while( prefix.length > 0 && c.substr(0, prefix.length) != prefix ) 185 | prefix = prefix.substr(0, -1); 186 | for( i in 0...cl.length ) 187 | cl[i] = cl[i].substr(prefix.length); 188 | } 189 | if( cl.length > 31 ) throw "Too many flags"; 190 | return cl; 191 | default: 192 | throw "Flags parameter should be an enum"; 193 | } 194 | } 195 | 196 | function makeType( t : haxe.macro.Type ) { 197 | switch( t ) { 198 | case TInst(c, _): 199 | var name = c.toString(); 200 | return switch( name ) { 201 | case "Int": DInt; 202 | case "Float": DFloat; 203 | case "String": DText; 204 | case "Date": DDateTime; 205 | case "haxe.io.Bytes": DBinary; 206 | default: throw "Unsupported Record Type " + name; 207 | } 208 | case TAbstract(a, p): 209 | var name = a.toString(); 210 | return switch( name ) { 211 | case "Int": DInt; 212 | case "Float": DFloat; 213 | case "Bool": DBool; 214 | case "Null": isNull = true; return makeType(p[0]); 215 | case _ if (!a.get().meta.has(':coreType')): 216 | var a = a.get(); 217 | #if macro 218 | makeType(a.type.applyTypeParameters(a.params, p)); 219 | #else 220 | makeType(a.type); 221 | #end 222 | default: throw "Unsupported Record Type " + name; 223 | } 224 | case TEnum(e, _): 225 | var name = e.toString(); 226 | return switch( name ) { 227 | case "Bool": DBool; 228 | default: 229 | throw "Unsupported Record Type " + name + " (enums must be wrapped with SData or SEnum)"; 230 | } 231 | case TType(td, p): 232 | var name = td.toString(); 233 | if( StringTools.startsWith(name, "sys.db.") ) 234 | name = name.substr(7); 235 | var k = g.types.get(name); 236 | if( k != null ) return k; 237 | if( p.length == 1 ) 238 | switch( name ) { 239 | case "SString": return DString(makeInt(p[0])); 240 | case "SBytes": return DBytes(makeInt(p[0])); 241 | case "SNull", "Null": isNull = true; return makeType(p[0]); 242 | case "SFlags": return DFlags(getFlags(p[0]),false); 243 | case "SSmallFlags": return DFlags(getFlags(p[0]),true); 244 | case "SData": return DData; 245 | case "SEnum": 246 | switch( p[0] ) { 247 | case TEnum(en, _): 248 | var e = en.get(); 249 | var count = 0, hasParam = false; 250 | for( c in e.constructs ) { 251 | count++; 252 | switch( c.type ) { 253 | case TFun(_): 254 | hasParam = true; 255 | default: 256 | } 257 | } 258 | if( hasParam ) 259 | throw "You can't use SEnum if the enum have parameters, try SData instead"; 260 | if( count >= 256 ) 261 | throw "Too many enum constructors"; 262 | return DEnum(en.toString()); 263 | default: 264 | // should cause another error 265 | } 266 | default: 267 | } 268 | return makeType(follow(t, true)); 269 | case TLazy(f): 270 | return makeType(f()); 271 | default: 272 | } 273 | throw "Unsupported Record Type " + Std.string(t); 274 | } 275 | 276 | function makeIdent( e : Expr ) { 277 | return switch( e.expr ) { 278 | case EConst(c): 279 | switch( c ) { 280 | case CIdent(s): s; 281 | default: error("Identifier expected", e.pos); 282 | } 283 | default: error("Identifier expected", e.pos); 284 | } 285 | } 286 | 287 | function getRecordInfos( c : haxe.macro.Type.Ref ) : RecordInfos { 288 | var cname = c.toString(); 289 | var i = g.cache.get(cname); 290 | if( i != null ) return i; 291 | i = { 292 | key : null, 293 | name : cname.split(".").pop(), // remove package name 294 | fields : [], 295 | hfields : new haxe.ds.StringMap(), 296 | relations : [], 297 | indexes : [], 298 | }; 299 | g.cache.set(cname, i); 300 | var c = c.get(); 301 | var fieldsPos = new haxe.ds.StringMap(); 302 | var fields = c.fields.get(); 303 | var csup = c.superClass; 304 | while( csup != null ) { 305 | var c = csup.t.get(); 306 | if( !c.meta.has(":skipFields") ) 307 | fields = c.fields.get().concat(fields); 308 | csup = c.superClass; 309 | } 310 | for( f in fields ) { 311 | fieldsPos.set(f.name, f.pos); 312 | switch( f.kind ) { 313 | case FMethod(_): 314 | // skip methods 315 | continue; 316 | case FVar(g, s): 317 | // skip not-db fields 318 | if( f.meta.has(":skip") ) 319 | continue; 320 | // handle relations 321 | if( f.meta.has(":relation") ) { 322 | if( !Type.enumEq(g,AccCall) || !Type.enumEq(s,AccCall) ) 323 | error("Relation should be (dynamic,dynamic)", f.pos); 324 | for( m in f.meta.get() ) { 325 | if( m.name != ":relation" ) continue; 326 | if( m.params.length == 0 ) error("Missing relation key", m.pos); 327 | var params = []; 328 | for( p in m.params ) 329 | params.push({ i : makeIdent(p), p : p.pos }); 330 | isNull = false; 331 | var t = makeRecord(f.type); 332 | if( t == null ) error("Relation type should be a sys.db.Object", f.pos); 333 | var mod = t.get().module; 334 | var r = { 335 | prop : f.name, 336 | key : params.shift().i, 337 | type : t.toString(), 338 | module : mod, 339 | cascade : false, 340 | lock : false, 341 | isNull : isNull, 342 | }; 343 | // setup flags 344 | for( p in params ) 345 | switch( p.i ) { 346 | case "lock": r.lock = true; 347 | case "cascade": r.cascade = true; 348 | default: error("Unknown relation flag", p.p); 349 | } 350 | i.relations.push(r); 351 | } 352 | continue; 353 | } 354 | switch( g ) { 355 | case AccCall: 356 | if( !f.meta.has(":data") ) 357 | error("Relation should be defined with @:relation(key)", f.pos); 358 | default: 359 | } 360 | } 361 | isNull = false; 362 | var fi = { 363 | name : f.name, 364 | t : try makeType(f.type) catch( e : String ) error(e,f.pos), 365 | isNull : isNull, 366 | }; 367 | var isId = switch( fi.t ) { 368 | case DId, DUId, DBigId: true; 369 | default: i.key == null && fi.name == "id"; 370 | } 371 | if( isId ) { 372 | switch(fi.t) 373 | { 374 | case DInt: 375 | fi.t = DId; 376 | case DUInt: 377 | fi.t = DUId; 378 | case DBigInt: 379 | fi.t = DBigId; 380 | case _: 381 | } 382 | if( i.key == null ) i.key = [fi.name] else error("Multiple table id declaration", f.pos); 383 | } 384 | i.fields.push(fi); 385 | i.hfields.set(fi.name, fi); 386 | } 387 | // create fields for undeclared relations keys : 388 | for( r in i.relations ) { 389 | var field = fields.find(function(f) return f.name == r.prop); 390 | var f = i.hfields.get(r.key); 391 | var relatedInf = getRecordInfos(makeRecord(resolveType(r.type, r.module))); 392 | if (relatedInf.key.length > 1) 393 | error('The relation ${r.prop} is invalid: Type ${r.type} has multiple keys, which is not supported',field.pos); 394 | var relatedKey = relatedInf.key[0]; 395 | var relatedKeyType = switch(relatedInf.hfields.get(relatedKey).t) 396 | { 397 | case DId: DInt; 398 | case DUId: DUInt; 399 | case DBigId: DBigInt; 400 | case t = DString(_): t; 401 | case t: error('Unexpected id type $t for the relation. Use either SId, SInt, SUId, SUInt, SBigID, SBigInt or SString', field.pos); 402 | } 403 | 404 | if( f == null ) { 405 | f = { 406 | name : r.key, 407 | t : relatedKeyType, 408 | isNull : r.isNull, 409 | }; 410 | i.fields.push(f); 411 | i.hfields.set(f.name, f); 412 | } else { 413 | var pos = fieldsPos.get(f.name); 414 | if( f.t != relatedKeyType) error("Relation source and field should have same type", pos); 415 | if( f.isNull != r.isNull ) error("Relation and field should have same nullability", pos); 416 | } 417 | } 418 | // process class metadata 419 | for( m in c.meta.get() ) 420 | switch( m.name ) { 421 | case ":id": 422 | i.key = []; 423 | for( p in m.params ) { 424 | var id = makeIdent(p); 425 | if( !i.hfields.exists(id) ) 426 | error("This field does not exists", p.pos); 427 | i.key.push(id); 428 | } 429 | if( i.key.length == 0 ) error("Invalid :id", m.pos); 430 | if (i.key.length == 1 ) 431 | { 432 | var field = i.hfields.get(i.key[0]); 433 | switch(field.t) 434 | { 435 | case DInt: 436 | field.t = DId; 437 | case DUInt: 438 | field.t = DUId; 439 | case DBigInt: 440 | field.t = DBigId; 441 | case _: 442 | } 443 | } 444 | case ":index": 445 | var idx = []; 446 | for( p in m.params ) idx.push(makeIdent(p)); 447 | var unique = idx[idx.length - 1] == "unique"; 448 | if( unique ) idx.pop(); 449 | if( idx.length == 0 ) error("Invalid :index", m.pos); 450 | for( k in 0...idx.length ) 451 | if( !i.hfields.exists(idx[k]) ) 452 | error("This field does not exists", m.params[k].pos); 453 | i.indexes.push( { keys : idx, unique : unique } ); 454 | case ":table": 455 | if( m.params.length != 1 ) error("Invalid :table", m.pos); 456 | i.name = switch( m.params[0].expr ) { 457 | case EConst(c): switch( c ) { case CString(s): s; default: null; } 458 | default: null; 459 | }; 460 | if( i.name == null ) error("Invalid :table value", m.params[0].pos); 461 | default: 462 | } 463 | // check primary key defined 464 | if( i.key == null ) 465 | error("Table is missing unique id, use either SId, SUId, SBigID or @:id", c.pos); 466 | return i; 467 | } 468 | 469 | function quoteField( f : String ) { 470 | return Manager.KEYWORDS.exists(f.toLowerCase()) ? "`"+f+"`" : f; 471 | } 472 | 473 | function initManager( pos : Position ) { 474 | manager = { expr : EField({ expr : EField({ expr : EConst(CIdent("sys")), pos : pos },"db"), pos : pos }, "Manager"), pos : pos }; 475 | } 476 | 477 | inline function makeString( s : String, pos ) { 478 | return { expr : EConst(CString(s)), pos : pos }; 479 | } 480 | 481 | inline function makeOp( op : String, e1, e2, pos ) { 482 | return sqlAdd(sqlAddString(e1,op),e2,pos); 483 | } 484 | 485 | inline function sqlAdd( e1 : Expr, e2 : Expr, pos : Position ) { 486 | return { expr : EBinop(OpAdd, e1, e2), pos : pos }; 487 | } 488 | 489 | inline function sqlAddString( sql : Expr, s : String ) { 490 | return { expr : EBinop(OpAdd, sql, makeString(s,sql.pos)), pos : sql.pos }; 491 | } 492 | 493 | function sqlQuoteValue( v : Expr, t : RecordType, isNull : Bool ) { 494 | switch( v.expr ) { 495 | case EConst(c): 496 | switch( c ) { 497 | case CInt(_), CFloat(_): return v; 498 | case CString(s): 499 | if( simpleString.match(s) ) return { expr : EConst(CString("'"+s+"'")), pos : v.pos }; 500 | case CIdent(n): 501 | switch( n ) { 502 | case "null": return { expr : EConst(CString("NULL")), pos : v.pos }; 503 | } 504 | default: 505 | } 506 | default: 507 | } 508 | return { expr : ECall( { expr : EField(manager, "quoteAny"), pos : v.pos }, [ensureType(v,t,isNull)]), pos : v.pos } 509 | } 510 | 511 | inline function sqlAddValue( sql : Expr, v : Expr, t : RecordType, isNull : Bool ) { 512 | return { expr : EBinop(OpAdd, sql, sqlQuoteValue(v,t, isNull)), pos : sql.pos }; 513 | } 514 | 515 | function unifyClass( t : RecordType ) { 516 | return switch( t ) { 517 | case DId, DInt, DUId, DUInt, DEncoded, DFlags(_), DTinyInt, DTinyUInt, DSmallInt, DSmallUInt, DMediumInt, DMediumUInt: 0; 518 | case DBigId, DBigInt, DSingle, DFloat: 1; 519 | case DBool: 2; 520 | case DString(_), DTinyText, DSmallText, DText, DSerialized: 3; 521 | case DDate, DDateTime, DTimeStamp: 4; 522 | case DSmallBinary, DLongBinary, DBinary, DBytes(_), DNekoSerialized, DData: 5; 523 | case DInterval: 6; 524 | case DNull: 7; 525 | case DEnum(_): -1; 526 | }; 527 | } 528 | 529 | function tryUnify( t, rt ) { 530 | if( t == rt ) return true; 531 | var c = unifyClass(t); 532 | if( c < 0 ) return Type.enumEq(t, rt); 533 | var rc = unifyClass(rt); 534 | return c == rc || (c == 0 && rc == 1); // allow Int-to-Float expansion 535 | } 536 | 537 | function typeStr( t : RecordType ) { 538 | return Std.string(t).substr(1); 539 | } 540 | 541 | function canStringify( t : RecordType ) { 542 | return switch( unifyClass(t) ) { 543 | case 0, 1, 2, 3, 4, 5, 7: true; 544 | default: false; 545 | }; 546 | } 547 | 548 | function convertType( t : RecordType ) { 549 | var pack = []; 550 | return TPath( { 551 | name : switch( unifyClass(t) ) { 552 | case 0: "Int"; 553 | case 1: "Float"; 554 | case 2: "Bool"; 555 | case 3: "String"; 556 | case 4: "Date"; 557 | case 5: pack = ["haxe", "io"]; "Bytes"; 558 | default: throw "assert"; 559 | }, 560 | pack : pack, 561 | params : [], 562 | sub : null, 563 | }); 564 | } 565 | 566 | function unify( t : RecordType, rt : RecordType, pos : Position ) { 567 | if( !tryUnify(t, rt) ) 568 | error(typeStr(t) + " should be " + typeStr(rt), pos); 569 | } 570 | 571 | function buildCmp( op, e1, e2, pos ) { 572 | var r1 = buildCond(e1); 573 | var r2 = buildCond(e2); 574 | unify(r2.t, r1.t, e2.pos); 575 | if( !tryUnify(r1.t, DFloat) && !tryUnify(r1.t, DDate) && !tryUnify(r1.t, DText) ) 576 | unify(r1.t, DFloat, e1.pos); 577 | return { sql : makeOp(op, r1.sql, r2.sql, pos), t : DBool, n : r1.n || r2.n }; 578 | } 579 | 580 | function buildNum( op, e1, e2, pos ) { 581 | var r1 = buildCond(e1); 582 | var r2 = buildCond(e2); 583 | var c1 = unifyClass(r1.t); 584 | var c2 = unifyClass(r2.t); 585 | if( c1 > 1 ) { 586 | if( op == "-" && tryUnify(r1.t, DDateTime) && tryUnify(r2.t,DInterval) ) 587 | return { sql : makeOp(op, r1.sql, r2.sql, pos), t : DDateTime, n : r1.n }; 588 | unify(r1.t, DInt, e1.pos); 589 | } 590 | if( c2 > 1 ) unify(r2.t, DInt, e2.pos); 591 | return { sql : makeOp(op, r1.sql, r2.sql, pos), t : (c1 + c2) == 0 ? DInt : DFloat, n : r1.n || r2.n }; 592 | } 593 | 594 | function buildInt( op, e1, e2, pos ) { 595 | var r1 = buildCond(e1); 596 | var r2 = buildCond(e2); 597 | unify(r1.t, DInt, e1.pos); 598 | unify(r2.t, DInt, e2.pos); 599 | return { sql : makeOp(op, r1.sql, r2.sql, pos), t : DInt, n : r1.n || r2.n }; 600 | } 601 | 602 | function buildEq( eq, e1 : Expr, e2, pos ) { 603 | var r1 = null; 604 | switch( e1.expr ) { 605 | case EConst(c): 606 | switch( c ) { 607 | case CIdent(i): 608 | if( i.charCodeAt(0) == "$".code ) { 609 | var tmp = { field : i.substr(1), expr : e2 }; 610 | var f = getField(tmp); 611 | r1 = { sql : makeString(quoteField(tmp.field), e1.pos), t : f.t, n : f.isNull }; 612 | e2 = tmp.expr; 613 | switch( f.t ) { 614 | case DEnum(e): 615 | var ok = false; 616 | switch( e2.expr ) { 617 | case EConst(c): 618 | switch( c ) { 619 | case CIdent(n): 620 | if( n.charCodeAt(0) == '$'.code ) 621 | ok = true; 622 | else switch( resolveType(e) ) { 623 | case TEnum(e, _): 624 | var c = e.get().constructs.get(n); 625 | if( c == null ) { 626 | if( n == "null" ) 627 | return { sql : sqlAddString(r1.sql, eq ? " IS NULL" : " IS NOT NULL"), t : DBool, n : false }; 628 | } else { 629 | return { sql : makeOp(eq?" = ":" != ", r1.sql, { expr : EConst(CInt(Std.string(c.index))), pos : e2.pos }, pos), t : DBool, n : r1.n }; 630 | } 631 | default: 632 | } 633 | default: 634 | } 635 | default: 636 | } 637 | if( !ok ) 638 | { 639 | var epath = e.split('.'); 640 | var ename = epath.pop(); 641 | var etype = TPath({ name:ename, pack:epath }); 642 | if (r1.n) { 643 | 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 }; 644 | } else { 645 | var expr = macro { @:pos(e2.pos) var tmp : $etype = $e2; (tmp == null ? null : (std.Type.enumIndex(tmp) + '')); }; 646 | return { sql: makeOp(eq?" = ":" != ", r1.sql, expr, pos), t : DBool, n : r1.n }; 647 | } 648 | } 649 | default: 650 | } 651 | } 652 | default: 653 | } 654 | default: 655 | } 656 | if( r1 == null ) 657 | r1 = buildCond(e1); 658 | var r2 = buildCond(e2); 659 | if( r2.t == DNull ) { 660 | if( !r1.n ) 661 | error("Expression can't be null", e1.pos); 662 | return { sql : sqlAddString(r1.sql, eq ? " IS NULL" : " IS NOT NULL"), t : DBool, n : false }; 663 | } else { 664 | unify(r2.t, r1.t, e2.pos); 665 | unify(r1.t, r2.t, e1.pos); 666 | } 667 | var sql; 668 | // use some different operators if there is a possibility for comparing two NULLs 669 | if( r1.n || r2.n ) 670 | sql = { expr : ECall({ expr : EField(manager,"nullCompare"), pos : pos },[r1.sql,r2.sql,{ expr : EConst(CIdent(eq?"true":"false")), pos : pos }]), pos : pos }; 671 | else 672 | sql = makeOp(eq?" = ":" != ", r1.sql, r2.sql, pos); 673 | return { sql : sql, t : DBool, n : r1.n || r2.n }; 674 | } 675 | 676 | function buildDefault( cond : Expr ) { 677 | var t = typeof(cond); 678 | isNull = false; 679 | var d = try makeType(t) catch( e : String ) try makeType(follow(t)) catch( e : String ) error("Unsupported type " + Std.string(t), cond.pos); 680 | return { sql : sqlQuoteValue(cond, d, isNull), t : d, n : isNull }; 681 | } 682 | 683 | function getField( f : { field : String, expr : Expr } ) { 684 | var fi = inf.hfields.get(f.field); 685 | if( fi == null ) { 686 | for( r in inf.relations ) 687 | if( r.prop == f.field ) { 688 | var path = r.type.split("."); 689 | var p = f.expr.pos; 690 | path.push("manager"); 691 | var first = path.shift(); 692 | var mpath = { expr : EConst(CIdent(first)), pos : p }; 693 | for ( e in path ) 694 | mpath = { expr : EField(mpath, e), pos : p }; 695 | var m = getManager(typeof(mpath),p); 696 | var getid = { expr : ECall( { expr : EField(mpath, "unsafeGetId"), pos : p }, [f.expr]), pos : p }; 697 | f.field = r.key; 698 | f.expr = ensureType(getid, m.inf.hfields.get(m.inf.key[0]).t, r.isNull); 699 | return inf.hfields.get(r.key); 700 | } 701 | error("No database field '" + f.field+"'", f.expr.pos); 702 | } 703 | return fi; 704 | } 705 | 706 | function buildCond( cond : Expr ) { 707 | var sql = null; 708 | var p = cond.pos; 709 | switch( cond.expr ) { 710 | case EBlock([]): 711 | return buildCond({ expr: EObjectDecl([]), pos:cond.pos }); 712 | case EObjectDecl(fl): 713 | var first = true; 714 | var sql = makeString("(", p); 715 | var fields = new haxe.ds.StringMap(); 716 | for( f in fl ) { 717 | var fi = getField(f); 718 | if( first ) 719 | first = false; 720 | else 721 | sql = sqlAddString(sql, " AND "); 722 | sql = sqlAddString(sql, quoteField(fi.name) + (fi.isNull ? " <=> " : " = ")); 723 | sql = sqlAddValue(sql, f.expr, fi.t, fi.isNull); 724 | if( fields.exists(fi.name) ) 725 | error("Duplicate field " + fi.name, p); 726 | else 727 | fields.set(fi.name, true); 728 | } 729 | if( first ) sql = sqlAdd(sql, sqlQuoteValue({ expr : EConst(CIdent("true")), pos : sql.pos }, DBool, false), sql.pos); 730 | sql = sqlAddString(sql, ")"); 731 | return { sql : sql, t : DBool, n : false }; 732 | case EParenthesis(e): 733 | var r = buildCond(e); 734 | r.sql = sqlAdd(makeString("(", p), r.sql, p); 735 | r.sql = sqlAddString(r.sql, ")"); 736 | return r; 737 | case EBinop(op, e1, e2): 738 | switch( op ) { 739 | case OpAdd: 740 | var r1 = buildCond(e1); 741 | var r2 = buildCond(e2); 742 | if( tryUnify(r1.t, DFloat) && tryUnify(r2.t, DFloat) ) { 743 | var rt = tryUnify(r1.t, DInt) ? tryUnify(r2.t, DInt) ? DInt : DFloat : DFloat; 744 | return { sql : makeOp("+", r1.sql, r2.sql, p), t : rt, n : r1.n || r2.n }; 745 | } else if( (tryUnify(r1.t, DText) && canStringify(r2.t)) || (tryUnify(r2.t, DText) && canStringify(r1.t)) ) { 746 | return { sql : makeOp("||", r1.sql, r2.sql, p), t : DText, n : r1.n || r2.n } 747 | } else { 748 | error("Can't add " + typeStr(r1.t) + " and " + typeStr(r2.t), p); 749 | } 750 | case OpBoolAnd, OpBoolOr: 751 | var r1 = buildCond(e1); 752 | var r2 = buildCond(e2); 753 | unify(r1.t, DBool, e1.pos); 754 | unify(r2.t, DBool, e2.pos); 755 | return { sql : makeOp(op == OpBoolAnd ? " AND " : " OR ", r1.sql, r2.sql, p), t : DBool, n : false }; 756 | case OpGte: 757 | return buildCmp(">=", e1, e2, p); 758 | case OpLte: 759 | return buildCmp("<=", e1, e2, p); 760 | case OpGt: 761 | return buildCmp(">", e1, e2, p); 762 | case OpLt: 763 | return buildCmp("<", e1, e2, p); 764 | case OpSub: 765 | return buildNum("-", e1, e2, p); 766 | case OpDiv: 767 | var r = buildNum("/", e1, e2, p); 768 | r.t = DFloat; 769 | return r; 770 | case OpMult: 771 | return buildNum("*", e1, e2, p); 772 | case OpEq, OpNotEq: 773 | return buildEq(op == OpEq, e1, e2, p); 774 | case OpXor: 775 | return buildInt("^", e1, e2, p); 776 | case OpOr: 777 | return buildInt("|", e1, e2, p); 778 | case OpAnd: 779 | return buildInt("&", e1, e2, p); 780 | case OpShr: 781 | return buildInt(">>", e1, e2, p); 782 | case OpShl: 783 | return buildInt("<<", e1, e2, p); 784 | case OpMod: 785 | return buildNum("%", e1, e2, p); 786 | #if (haxe_ver >= 4) 787 | case OpIn: 788 | var e = buildCond(e1); 789 | var t = TPath({ 790 | pack : [], 791 | name : "Iterable", 792 | params : [TPType(convertType(e.t))], 793 | sub : null, 794 | }); 795 | 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 }; 796 | #end 797 | case OpUShr, OpInterval, OpAssignOp(_), OpAssign, OpArrow #if (haxe_ver >= 4.3) , OpNullCoal #end: 798 | error("Unsupported operation", p); 799 | } 800 | case EUnop(op, _, e): 801 | var r = buildCond(e); 802 | switch( op ) { 803 | case OpNot: 804 | var sql = makeString("!", p); 805 | unify(r.t, DBool, e.pos); 806 | switch( r.sql.expr ) { 807 | case EConst(_): 808 | default: 809 | r.sql = sqlAddString(r.sql, ")"); 810 | sql = sqlAddString(sql, "("); 811 | } 812 | return { sql : sqlAdd(sql, r.sql, p), t : DBool, n : r.n }; 813 | case OpNegBits: 814 | var sql = makeString("~", p); 815 | unify(r.t, DInt, e.pos); 816 | return { sql : sqlAdd(sql, r.sql, p), t : DInt, n : r.n }; 817 | case OpNeg: 818 | var sql = makeString("-", p); 819 | unify(r.t, DFloat, e.pos); 820 | return { sql : sqlAdd(sql, r.sql, p), t : r.t, n : r.n }; 821 | case OpIncrement, OpDecrement #if (haxe_ver >= "4.2") , OpSpread #end: 822 | error("Unsupported operation", p); 823 | } 824 | case EConst(c): 825 | switch( c ) { 826 | case CInt(s): return { sql : makeString(s, p), t : DInt, n : false }; 827 | case CFloat(s): return { sql : makeString(s, p), t : DFloat, n : false }; 828 | case CString(s): return { sql : sqlQuoteValue(cond, DText, false), t : DString(s.length), n : false }; 829 | case CRegexp(_): error("Unsupported", p); 830 | case CIdent(n): 831 | if( n.charCodeAt(0) == "$".code ) { 832 | n = n.substr(1); 833 | var f = inf.hfields.get(n); 834 | if( f == null ) error("Unknown database field '" + n + "'", p); 835 | return { sql : makeString(quoteField(f.name), p), t : f.t, n : f.isNull }; 836 | } 837 | switch( n ) { 838 | case "null": 839 | return { sql : makeString("NULL", p), t : DNull, n : true }; 840 | } 841 | return buildDefault(cond); 842 | } 843 | case ECall(c, pl): 844 | switch( c.expr ) { 845 | case EConst(co): 846 | switch(co) { 847 | case CIdent(t): 848 | if( t.charCodeAt(0) == '$'.code ) { 849 | var f = g.functions.get(t.substr(1)); 850 | if( f == null ) error("Unknown method " + t, c.pos); 851 | if( f.params.length != pl.length ) error("Function " + f.name + " requires " + f.params.length + " parameters", p); 852 | var parts = f.sql.split("$"); 853 | var sql = makeString(parts[0], p); 854 | var first = true; 855 | var isNull = false; 856 | for( i in 0...f.params.length ) { 857 | var r = buildCond(pl[i]); 858 | if( r.n ) isNull = true; 859 | unify(r.t, f.params[i], pl[i].pos); 860 | if( first ) 861 | first = false; 862 | else 863 | sql = sqlAddString(sql, ","); 864 | sql = sqlAdd(sql, r.sql, p); 865 | } 866 | sql = sqlAddString(sql, parts[1]); 867 | // assume that for all SQL functions, a NULL parameter will make a NULL result 868 | return { sql : sql, t : f.ret, n : isNull }; 869 | } 870 | default: 871 | } 872 | case EField(e, f): 873 | switch( f ) { 874 | case "like": 875 | if( pl.length == 1 ) { 876 | var r = buildCond(e); 877 | var v = buildCond(pl[0]); 878 | if( !tryUnify(r.t, DText) ) { 879 | if( tryUnify(r.t, DBinary) ) 880 | unify(v.t, DBinary, pl[0].pos); 881 | else 882 | unify(r.t, DText, e.pos); 883 | } else 884 | unify(v.t, DText, pl[0].pos); 885 | return { sql : makeOp(" LIKE ", r.sql, v.sql, p), t : DBool, n : r.n || v.n }; 886 | } 887 | case "has": 888 | if( pl.length == 1 ) { 889 | var r = buildCond(e); 890 | switch( r.t ) { 891 | case DFlags(vals,_): 892 | var id = makeIdent(pl[0]); 893 | var idx = Lambda.indexOf(vals,id); 894 | if( idx < 0 ) error("Flag should be "+vals.join(","), pl[0].pos); 895 | return { sql : sqlAddString(r.sql, " & " + (1 << idx) + " != 0"), t : DBool, n : r.n }; 896 | default: 897 | } 898 | } 899 | } 900 | default: 901 | } 902 | return buildDefault(cond); 903 | case EField(_, _), EDisplay(_): 904 | return buildDefault(cond); 905 | case EIf(e, e1, e2), ETernary(e, e1, e2): 906 | if( e2 == null ) error("If must have an else statement", p); 907 | var r1 = buildCond(e1); 908 | var r2 = buildCond(e2); 909 | unify(r2.t, r1.t, e2.pos); 910 | unify(r1.t, r2.t, e1.pos); 911 | return { sql : { expr : EIf(e, r1.sql, r2.sql), pos : p }, t : r1.t, n : r1.n || r2.n }; 912 | #if (haxe_ver < 4) 913 | case EIn(e, v): 914 | var e = buildCond(e); 915 | var t = TPath({ 916 | pack : [], 917 | name : "Iterable", 918 | params : [TPType(convertType(e.t))], 919 | sub : null, 920 | }); 921 | 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 }; 922 | #end 923 | default: 924 | return buildDefault(cond); 925 | } 926 | error("Unsupported expression", p); 927 | return null; 928 | } 929 | 930 | function ensureType( e : Expr, rt : RecordType, isNull : Bool ) { 931 | var t = convertType(rt); 932 | if (isNull) { 933 | t = macro : Null<$t>; 934 | } 935 | return { expr : ECheckType(e, t), pos : e.pos }; 936 | } 937 | 938 | function checkKeys( econd : Expr ) { 939 | var p = econd.pos; 940 | switch( econd.expr ) { 941 | case EObjectDecl(fl): 942 | var key = inf.key.copy(); 943 | for( f in fl ) { 944 | var fi = getField(f); 945 | if( !key.remove(fi.name) ) { 946 | if( Lambda.has(inf.key, fi.name) ) 947 | error("Duplicate field " + fi.name, p); 948 | else 949 | error("Field " + f.field + " is not part of table key (" + inf.key.join(",") + ")", p); 950 | } 951 | f.expr = ensureType(f.expr, fi.t, fi.isNull); 952 | } 953 | return econd; 954 | default: 955 | if( inf.key.length > 1 ) 956 | error("You can't use a single value on a table with multiple keys (" + inf.key.join(",") + ")", p); 957 | var fi = inf.hfields.get(inf.key[0]); 958 | return ensureType(econd, fi.t, fi.isNull); 959 | } 960 | } 961 | 962 | function orderField(e) { 963 | switch( e.expr ) { 964 | case EConst(c): 965 | switch( c ) { 966 | case CIdent(t): 967 | if( !inf.hfields.exists(t) ) 968 | error("Unknown database field", e.pos); 969 | return quoteField(t); 970 | default: 971 | } 972 | case EUnop(op, _, e): 973 | if( op == OpNeg ) 974 | return orderField(e) + " DESC"; 975 | default: 976 | } 977 | error("Invalid order field", e.pos); 978 | return null; 979 | } 980 | 981 | function concatStrings( e : Expr ) { 982 | var inf = { e : null, str : null }; 983 | browseStrings(inf, e); 984 | if( inf.str != null ) { 985 | var es = { expr : EConst(CString(inf.str)), pos : e.pos }; 986 | if( inf.e == null ) 987 | inf.e = es; 988 | else 989 | inf.e = { expr : EBinop(OpAdd, inf.e, es), pos : e.pos }; 990 | } 991 | return inf.e; 992 | } 993 | 994 | function browseStrings( inf : { e : Expr, str : String }, e : Expr ) { 995 | switch( e.expr ) { 996 | case EConst(c): 997 | switch( c ) { 998 | case CString(s): 999 | if( inf.str == null ) 1000 | inf.str = s; 1001 | else 1002 | inf.str += s; 1003 | return; 1004 | case CInt(s), CFloat(s): 1005 | if( inf.str != null ) { 1006 | inf.str += s; 1007 | return; 1008 | } 1009 | default: 1010 | } 1011 | case EBinop(op, e1, e2): 1012 | if( op == OpAdd ) { 1013 | browseStrings(inf,e1); 1014 | browseStrings(inf,e2); 1015 | return; 1016 | } 1017 | case EIf(cond, e1, e2): 1018 | e = { expr : EIf(cond, concatStrings(e1), concatStrings(e2)), pos : e.pos }; 1019 | default: 1020 | } 1021 | if( inf.str != null ) { 1022 | e = { expr : EBinop(OpAdd, { expr : EConst(CString(inf.str)), pos : e.pos }, e), pos : e.pos }; 1023 | inf.str = null; 1024 | } 1025 | if( inf.e == null ) 1026 | inf.e = e; 1027 | else 1028 | inf.e = { expr : EBinop(OpAdd, inf.e, e), pos : e.pos }; 1029 | } 1030 | 1031 | function buildOptions( eopt : Expr ) { 1032 | var p = eopt.pos; 1033 | var opts = new haxe.ds.StringMap(); 1034 | var opt = { limit : null, orderBy : null, forceIndex : null }; 1035 | switch( eopt.expr ) { 1036 | case EObjectDecl(fields): 1037 | var limit = null; 1038 | for( o in fields ) { 1039 | if( opts.exists(o.field) ) error("Duplicate option " + o.field, p); 1040 | opts.set(o.field, true); 1041 | switch( o.field ) { 1042 | case "orderBy": 1043 | var fields = switch( o.expr.expr ) { 1044 | case EArrayDecl(vl): Lambda.array(Lambda.map(vl, orderField)); 1045 | case ECall(v, pl): 1046 | if( pl.length != 0 || !Type.enumEq(v.expr, EConst(CIdent("rand"))) ) 1047 | [orderField(o.expr)] 1048 | else 1049 | ["RAND()"]; 1050 | default: [orderField(o.expr)]; 1051 | }; 1052 | opt.orderBy = fields.join(","); 1053 | case "limit": 1054 | var limits = switch( o.expr.expr ) { 1055 | case EArrayDecl(vl): Lambda.array(Lambda.map(vl, buildDefault)); 1056 | default: [buildDefault(o.expr)]; 1057 | } 1058 | if( limits.length == 0 || limits.length > 2 ) error("Invalid limits", o.expr.pos); 1059 | var l0 = limits[0], l1 = limits[1]; 1060 | unify(l0.t, DInt, l0.sql.pos); 1061 | if( l1 != null ) 1062 | unify(l1.t, DInt, l1.sql.pos); 1063 | opt.limit = { pos : l0.sql, len : l1 == null ? null : l1.sql }; 1064 | case "forceIndex": 1065 | var fields = switch( o.expr.expr ) { 1066 | case EArrayDecl(vl): Lambda.array(Lambda.map(vl, makeIdent)); 1067 | default: [makeIdent(o.expr)]; 1068 | } 1069 | for( f in fields ) 1070 | if( !inf.hfields.exists(f) ) 1071 | error("Unknown field " + f, o.expr.pos); 1072 | var idx = fields.join(","); 1073 | if( !Lambda.exists(inf.indexes, function(i) return i.keys.join(",") == idx) && !Lambda.exists(inf.relations,function(r) return r.key == idx) ) 1074 | error("These fields are not indexed", o.expr.pos); 1075 | opt.forceIndex = idx; 1076 | default: 1077 | error("Unknown option '" + o.field + "'", p); 1078 | } 1079 | } 1080 | default: 1081 | error("Options should be { orderBy : field, limit : [a,b] }", p); 1082 | } 1083 | return opt; 1084 | } 1085 | 1086 | public static function getInfos( t : haxe.macro.Type ) { 1087 | var c = switch( t ) { 1088 | case TInst(c, _): if( c.toString() == "sys.db.Object" ) return null; c; 1089 | default: return null; 1090 | }; 1091 | return new RecordMacros(c); 1092 | } 1093 | 1094 | 1095 | #if macro 1096 | static var RTTI = false; 1097 | static var FIRST_COMPILATION = true; 1098 | 1099 | public static function addRtti() : Array { 1100 | if( RTTI ) return null; 1101 | RTTI = true; 1102 | #if (haxe_ver < 4) 1103 | if( FIRST_COMPILATION ) { 1104 | FIRST_COMPILATION = false; 1105 | Context.onMacroContextReused(function() { 1106 | RTTI = false; 1107 | GLOBAL = null; 1108 | return true; 1109 | }); 1110 | } 1111 | #end 1112 | Context.getType("sys.db.RecordInfos"); 1113 | Context.onGenerate(function(types) { 1114 | for( t in types ) 1115 | switch( t ) { 1116 | case TInst(c, _): 1117 | var c = c.get(); 1118 | var cur = c.superClass; 1119 | while( cur != null ) { 1120 | if( cur.t.toString() == "sys.db.Object" ) 1121 | break; 1122 | cur = cur.t.get().superClass; 1123 | } 1124 | if( cur == null || c.meta.has(":skip") || c.meta.has("rtti") ) 1125 | continue; 1126 | var inst = getInfos(t); 1127 | var s = new haxe.Serializer(); 1128 | s.useEnumIndex = true; 1129 | s.useCache = true; 1130 | s.serialize(inst.inf); 1131 | c.meta.add("rtti", [ { expr : EConst(CString(s.toString())), pos : c.pos } ], c.pos); 1132 | default: 1133 | } 1134 | }); 1135 | return null; 1136 | } 1137 | 1138 | static function getManagerInfos( t : haxe.macro.Type, pos ) { 1139 | var param = null; 1140 | switch( t ) { 1141 | case TInst(c, p): 1142 | while( true ) { 1143 | if( c.toString() == "sys.db.Manager" ) { 1144 | param = p[0]; 1145 | break; 1146 | } 1147 | var csup = c.get().superClass; 1148 | if( csup == null ) break; 1149 | c = csup.t; 1150 | p = csup.params; 1151 | } 1152 | case TType(t, p): 1153 | if( p.length == 1 && t.toString() == "sys.db.Manager" ) 1154 | param = p[0]; 1155 | default: 1156 | } 1157 | var inst = if( param == null ) null else getInfos(param); 1158 | if( inst == null ) 1159 | Context.error("This method must be called from a specific Manager", Context.currentPos()); 1160 | inst.initManager(pos); 1161 | return inst; 1162 | } 1163 | 1164 | static function buildSQL( em : Expr, econd : Expr, prefix : String, ?eopt : Expr ) { 1165 | var pos = Context.currentPos(); 1166 | var inst = getManagerInfos(Context.typeof(em),pos); 1167 | var sql = { expr : EConst(CString(prefix + " " + inst.quoteField(inst.inf.name))), pos : econd.pos }; 1168 | var r = inst.buildCond(econd); 1169 | if( r.t != DBool ) Context.error("Expression should be a condition", econd.pos); 1170 | if( eopt != null && !Type.enumEq(eopt.expr, EConst(CIdent("null"))) ) { 1171 | var opt = inst.buildOptions(eopt); 1172 | if( opt.orderBy != null ) 1173 | r.sql = inst.sqlAddString(r.sql, " ORDER BY " + opt.orderBy); 1174 | if( opt.limit != null ) { 1175 | r.sql = inst.sqlAddString(r.sql, " LIMIT "); 1176 | r.sql = inst.sqlAdd(r.sql, opt.limit.pos, pos); 1177 | if( opt.limit.len != null ) { 1178 | r.sql = inst.sqlAddString(r.sql, ","); 1179 | r.sql = inst.sqlAdd(r.sql, opt.limit.len, pos); 1180 | } 1181 | } 1182 | if( opt.forceIndex != null ) 1183 | sql = inst.sqlAddString(sql, " FORCE INDEX (" + inst.inf.name+"_"+opt.forceIndex+")"); 1184 | } 1185 | sql = inst.sqlAddString(sql, " WHERE "); 1186 | var sql = inst.sqlAdd(sql, r.sql, sql.pos); 1187 | #if !display 1188 | sql = inst.concatStrings(sql); 1189 | #end 1190 | return sql; 1191 | } 1192 | 1193 | public static function macroGet( em : Expr, econd : Expr, elock : Expr ) { 1194 | var pos = Context.currentPos(); 1195 | var inst = getManagerInfos(Context.typeof(em),pos); 1196 | econd = inst.checkKeys(econd); 1197 | elock = defaultTrue(elock); 1198 | switch( econd.expr ) { 1199 | case EObjectDecl(_): 1200 | return { expr : ECall({ expr : EField(em,"unsafeGetWithKeys"), pos : pos },[econd,elock]), pos : pos }; 1201 | default: 1202 | return { expr : ECall({ expr : EField(em,"unsafeGet"), pos : pos },[econd,elock]), pos : pos }; 1203 | } 1204 | } 1205 | 1206 | static function defaultTrue( e : Expr ) { 1207 | return switch( e.expr ) { 1208 | case EConst(CIdent("null")): { expr : EConst(CIdent("true")), pos : e.pos }; 1209 | default: e; 1210 | } 1211 | } 1212 | 1213 | public static function macroSearch( em : Expr, econd : Expr, eopt : Expr, elock : Expr, ?single ) { 1214 | // allow both search(e,opts) and search(e,lock) 1215 | if( eopt != null && (elock == null || Type.enumEq(elock.expr, EConst(CIdent("null")))) ) { 1216 | switch( eopt.expr ) { 1217 | case EObjectDecl(_): 1218 | default: 1219 | var tmp = eopt; 1220 | eopt = elock; 1221 | elock = tmp; 1222 | } 1223 | } 1224 | var sql = buildSQL(em, econd, "SELECT * FROM", eopt); 1225 | var pos = Context.currentPos(); 1226 | var e = { expr : ECall( { expr : EField(em, "unsafeObjects"), pos : pos }, [sql,defaultTrue(elock)]), pos : pos }; 1227 | if( single ) 1228 | e = { expr : ECall( { expr : EField(e, "first"), pos : pos }, []), pos : pos }; 1229 | return e; 1230 | } 1231 | 1232 | public static function macroCount( em : Expr, econd : Expr ) { 1233 | var sql = buildSQL(em, econd, "SELECT COUNT(*) FROM"); 1234 | var pos = Context.currentPos(); 1235 | return { expr : ECall({ expr : EField(em,"unsafeCount"), pos : pos },[sql]), pos : pos }; 1236 | } 1237 | 1238 | public static function macroDelete( em : Expr, econd : Expr, eopt : Expr ) { 1239 | var sql = buildSQL(em, econd, "DELETE FROM", eopt); 1240 | var pos = Context.currentPos(); 1241 | return { expr : ECall({ expr : EField(em,"unsafeDelete"), pos : pos },[sql]), pos : pos }; 1242 | } 1243 | 1244 | static var isNeko = Context.defined("neko"); 1245 | 1246 | static function buildField( f : Field, fields : Array, ft : ComplexType, rt : ComplexType, isNull=false ) { 1247 | var p = switch( ft ) { 1248 | case TPath(p): p; 1249 | default: return; 1250 | } 1251 | if( p.params.length != 1 ) 1252 | return; 1253 | var t = switch( p.params[0] ) { 1254 | case TPExpr(_): return; 1255 | case TPType(t): t; 1256 | }; 1257 | var pos = f.pos; 1258 | switch( p.sub != null ? p.sub : p.name ) { 1259 | case "SData": 1260 | f.kind = FProp("dynamic", "dynamic", rt, null); 1261 | f.meta.push( { name : ":data", params : [], pos : f.pos } ); 1262 | f.meta.push( { name : ":isVar", params : [], pos : f.pos } ); 1263 | var meta = [ { name : ":hide", params : [], pos : pos } ]; 1264 | var cache = "cache_" + f.name, 1265 | dataName = "data_" + f.name; 1266 | var ecache = { expr : EConst(CIdent(cache)), pos : pos }; 1267 | var efield = { expr : EConst(CIdent(dataName)), pos : pos }; 1268 | var fname = { expr : EConst(CString(dataName)), pos : pos }; 1269 | var get = { 1270 | args : [], 1271 | params : [], 1272 | ret : t, 1273 | // we set efield to an empty object to make sure it will be != from previous value when insert/update is triggered 1274 | expr : macro { if( $ecache == null ) { $ecache = { v : untyped manager.doUnserialize($fname, cast $efield) }; Reflect.setField(this, $fname, { } ); }; return $ecache.v; }, 1275 | }; 1276 | var set = { 1277 | args : [{ name : "_v", opt : false, type : t, value : null }], 1278 | params : [], 1279 | ret : t, 1280 | expr : macro { if( $ecache == null ) { $ecache = { v : _v }; $efield = cast {}; } else $ecache.v = _v; return _v; }, 1281 | }; 1282 | fields.push( { name : cache, pos : pos, meta : [meta[0], { name:":skip", params:[], pos:pos } ], access : [APrivate], doc : null, kind : FVar(macro : { v : $t }, null) } ); 1283 | fields.push( { name : dataName, pos : pos, meta : [meta[0], { name:":skip", params:[], pos:pos } ], access : [APrivate], doc : null, kind : FVar(macro : Dynamic, null) } ); 1284 | fields.push( { name : "get_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(get) } ); 1285 | fields.push( { name : "set_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(set) } ); 1286 | case "SEnum": 1287 | f.kind = FProp("dynamic", "dynamic", rt, null); 1288 | f.meta.push( { name : ":data", params : [], pos : f.pos } ); 1289 | var meta = [ { name : ":hide", params : [], pos : pos } ]; 1290 | var dataName = "data_" + f.name; 1291 | var efield = { expr : EConst(CIdent(dataName)), pos : pos }; 1292 | var eval = switch( t ) { 1293 | case TPath(p): 1294 | var pack = p.pack.copy(); 1295 | pack.push(p.name); 1296 | if( p.sub != null ) pack.push(p.sub); 1297 | Context.parse(pack.join("."), f.pos); 1298 | default: 1299 | Context.error("Enum parameter expected", f.pos); 1300 | } 1301 | var get = { 1302 | args : [], 1303 | params : [], 1304 | ret : t, 1305 | expr : macro return $efield == null ? null : Type.createEnumIndex($eval,cast $efield), 1306 | }; 1307 | var set = { 1308 | args : [{ name : "_v", opt : false, type : t, value : null }], 1309 | params : [], 1310 | ret : t, 1311 | expr : (Context.defined('cs') && !isNull) ? 1312 | macro { $efield = cast Type.enumIndex(_v); return _v; } : 1313 | macro { $efield = _v == null ? null : cast Type.enumIndex(_v); return _v; }, 1314 | }; 1315 | fields.push( { name : "get_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(get) } ); 1316 | fields.push( { name : "set_" + f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(set) } ); 1317 | fields.push( { name : dataName, pos : pos, meta : [meta[0], { name:":skip", params:[], pos:pos } ], access : [APrivate], doc : null, kind : FVar(macro : Null, null) } ); 1318 | case "SNull", "Null": 1319 | buildField(f, fields, t, rt,true); 1320 | } 1321 | } 1322 | 1323 | public static function macroBuild() { 1324 | var fields = Context.getBuildFields(); 1325 | var hasManager = false; 1326 | for( f in fields ) { 1327 | var skip = false; 1328 | if( f.name == "manager") hasManager = true; 1329 | for( m in f.meta ) 1330 | switch( m.name ) { 1331 | case ":skip": 1332 | skip = true; 1333 | case ":relation": 1334 | switch( f.kind ) { 1335 | case FVar(t, _): 1336 | f.kind = FProp("dynamic", "dynamic", t); 1337 | // create compile-time getter/setter for all platforms 1338 | var relKey = null; 1339 | var relParams = []; 1340 | var lock = false; 1341 | for( p in m.params ) 1342 | switch( p.expr ) { 1343 | case EConst(c): 1344 | switch( c ) { 1345 | case CIdent(i): 1346 | relParams.push(i); 1347 | default: 1348 | } 1349 | default: 1350 | } 1351 | relKey = relParams.shift(); 1352 | for( p in relParams ) 1353 | if( p == "lock" ) 1354 | lock = true; 1355 | // we will get an error later 1356 | if( relKey == null ) 1357 | continue; 1358 | // generate get/set methods stubs 1359 | var pos = f.pos; 1360 | var ttype = t, tname; 1361 | while( true ) 1362 | switch(ttype) { 1363 | case TPath(t): 1364 | if( t.params.length == 1 && (t.name == "Null" || t.name == "SNull") ) { 1365 | ttype = switch( t.params[0] ) { 1366 | case TPType(t): t; 1367 | default: throw "assert"; 1368 | }; 1369 | continue; 1370 | } 1371 | var p = t.pack.copy(); 1372 | p.push(t.name); 1373 | if( t.sub != null ) p.push(t.sub); 1374 | tname = p.join("."); 1375 | break; 1376 | default: 1377 | Context.error("Relation type should be a type path", f.pos); 1378 | } 1379 | function e(expr) return { expr : expr, pos : pos }; 1380 | var get = { 1381 | args : [], 1382 | params : [], 1383 | ret : t, 1384 | expr: macro return @:privateAccess $i{tname}.manager.__get(this, $v{f.name}, $v{relKey}, $v{lock}), 1385 | }; 1386 | var set = { 1387 | args : [{ name : "_v", opt : false, type : t, value : null }], 1388 | params : [], 1389 | ret : t, 1390 | expr: macro return @:privateAccess $i{tname}.manager.__set(this, $v{f.name}, $v{relKey}, _v), 1391 | }; 1392 | var meta = [{ name : ":hide", params : [], pos : pos }]; 1393 | f.meta.push({ name: ":isVar", params : [], pos : pos }); 1394 | fields.push({ name : "get_"+f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(get) }); 1395 | fields.push({ name : "set_"+f.name, pos : pos, meta : meta, access : [APrivate], doc : null, kind : FFun(set) }); 1396 | var hasRelKey = false; 1397 | for( f in fields ) 1398 | if( f.name == relKey ) 1399 | { 1400 | hasRelKey = true; 1401 | if (f.meta == null) 1402 | f.meta = []; 1403 | f.meta.push({ name : ":skip", params : [], pos : pos }); 1404 | switch(f.kind) { 1405 | case FProp(_, _, t, e), FVar(t, e): 1406 | f.kind = FProp('default','never', t, e); 1407 | case FFun(f): 1408 | Context.error("Relation key should be a var or a property", f.expr.pos); 1409 | } 1410 | break; 1411 | } 1412 | if( !hasRelKey ) 1413 | fields.push({ name : relKey, pos : pos, meta : [{ name : ":skip", params : [], pos : pos }], access : [APrivate], doc : null, kind : FVar(macro : Dynamic) }); 1414 | default: 1415 | Context.error("Invalid relation field type", f.pos); 1416 | } 1417 | break; 1418 | default: 1419 | } 1420 | if( skip ) 1421 | continue; 1422 | switch( f.kind ) { 1423 | case FVar(t, _) | FProp('default',_,t,_): 1424 | if( t != null ) 1425 | buildField(f,fields,t,t); 1426 | default: 1427 | } 1428 | } 1429 | if( !hasManager ) { 1430 | var inst = Context.getLocalClass().get(); 1431 | if( inst.meta.has(":skip") ) 1432 | return fields; 1433 | if (!isNeko) 1434 | { 1435 | var iname = { expr:EConst(CIdent(inst.name)), pos: inst.pos }; 1436 | var getM = { 1437 | args : [], 1438 | params : [], 1439 | ret : macro : sys.db.Manager, 1440 | expr : macro return $iname.manager 1441 | }; 1442 | fields.push({ name: "__getManager", meta : [], access : [APrivate,AOverride], doc : null, kind : FFun(getM), pos : inst.pos }); 1443 | } 1444 | var p = inst.pos; 1445 | var tinst = TPath( { pack : inst.pack, name : inst.name, sub : null, params : [] } ); 1446 | var path = inst.pack.copy().concat([inst.name]).join("."); 1447 | var enew = { expr : ENew( { pack : ["sys", "db"], name : "Manager", sub : null, params : [TPType(tinst)] }, [Context.parse(path, p)]), pos : p } 1448 | fields.push({ name : "manager", meta : [], kind : FVar(null,enew), doc : null, access : [AStatic,APublic], pos : p }); 1449 | } 1450 | addRtti(); 1451 | return fields; 1452 | } 1453 | 1454 | #end 1455 | 1456 | } 1457 | -------------------------------------------------------------------------------- /src/sys/db/TableCreate.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2005-2016 Haxe Foundation 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | package sys.db; 23 | import sys.db.RecordInfos; 24 | 25 | class TableCreate { 26 | 27 | static function autoInc( dbName ) { 28 | // on SQLite, autoincrement is necessary to be primary key as well 29 | return dbName == "SQLite" ? "PRIMARY KEY AUTOINCREMENT" : "AUTO_INCREMENT"; 30 | } 31 | 32 | public static function getTypeSQL( t : RecordType, dbName : String ) { 33 | return switch( t ) { 34 | case DId: "INTEGER "+autoInc(dbName); 35 | case DUId: "INTEGER UNSIGNED "+autoInc(dbName); 36 | case DInt, DEncoded: "INTEGER"; 37 | case DUInt: "INTEGER UNSIGNED"; 38 | case DTinyInt: "TINYINT"; 39 | case DTinyUInt, DEnum(_): "TINYINT UNSIGNED"; 40 | case DSmallInt: "SMALLINT"; 41 | case DSmallUInt: "SMALLINT UNSIGNED"; 42 | case DMediumInt: "MEDIUMINT"; 43 | case DMediumUInt: "MEDIUMINT UNSIGNED"; 44 | case DSingle: "FLOAT"; 45 | case DFloat: "DOUBLE"; 46 | case DBool: "TINYINT(1)"; 47 | case DString(n): "VARCHAR("+n+")"; 48 | case DDate: "DATE"; 49 | case DDateTime: "DATETIME"; 50 | case DTimeStamp: "TIMESTAMP DEFAULT 0"; 51 | case DTinyText: "TINYTEXT"; 52 | case DSmallText: "TEXT"; 53 | case DText, DSerialized: "MEDIUMTEXT"; 54 | case DSmallBinary: "BLOB"; 55 | case DBinary, DNekoSerialized, DData: "MEDIUMBLOB"; 56 | case DLongBinary: "LONGBLOB"; 57 | case DBigInt: "BIGINT"; 58 | case DBigId: "BIGINT "+autoInc(dbName); 59 | case DBytes(n): "BINARY(" + n + ")"; 60 | case DFlags(fl, auto): getTypeSQL(auto ? (fl.length <= 8 ? DTinyUInt : (fl.length <= 16 ? DSmallUInt : (fl.length <= 24 ? DMediumUInt : DInt))) : DInt, dbName); 61 | case DNull, DInterval: throw "assert"; 62 | }; 63 | } 64 | 65 | public static function create( manager : sys.db.Manager, ?engine ) { 66 | function quote(v:String):String { 67 | return untyped manager.quoteField(v); 68 | } 69 | var cnx : Connection = untyped manager.getCnx(); 70 | if( cnx == null ) 71 | throw "SQL Connection not initialized on Manager"; 72 | var dbName = cnx.dbName(); 73 | var infos = manager.dbInfos(); 74 | var sql = "CREATE TABLE " + quote(infos.name) + " ("; 75 | var decls = []; 76 | var hasID = false; 77 | for( f in infos.fields ) { 78 | switch( f.t ) { 79 | case DId: 80 | hasID = true; 81 | case DUId, DBigId: 82 | hasID = true; 83 | if( dbName == "SQLite" ) 84 | throw "S" + Std.string(f.t).substr(1)+" is not supported by " + dbName + " : use SId instead"; 85 | default: 86 | } 87 | decls.push(quote(f.name)+" "+getTypeSQL(f.t,dbName)+(f.isNull ? "" : " NOT NULL")); 88 | } 89 | if( dbName != "SQLite" || !hasID ) 90 | decls.push("PRIMARY KEY ("+Lambda.map(infos.key,quote).join(",")+")"); 91 | sql += decls.join(","); 92 | sql += ")"; 93 | if( engine != null ) 94 | sql += "ENGINE="+engine; 95 | cnx.request(sql); 96 | } 97 | 98 | public static function exists( manager : sys.db.Manager ) : Bool { 99 | var cnx : Connection = untyped manager.getCnx(); 100 | if( cnx == null ) 101 | throw "SQL Connection not initialized on Manager"; 102 | try { 103 | cnx.request("SELECT * FROM `"+manager.dbInfos().name+"` LIMIT 1"); 104 | return true; 105 | } catch( e : Dynamic ) { 106 | return false; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/sys/db/Transaction.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2005-2017 Haxe Foundation 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | package sys.db; 23 | 24 | class Transaction { 25 | 26 | public static function isDeadlock(e : Dynamic) { 27 | return Std.is(e,String) && ~/try restarting transaction/.match(e); 28 | } 29 | 30 | private static function runMainLoop(mainFun,logError,count) { 31 | try { 32 | mainFun(); 33 | } catch( e : Dynamic ) { 34 | if( count > 0 && isDeadlock(e) ) { 35 | Manager.cleanup(); 36 | Manager.cnx.rollback(); // should be already done, but in case... 37 | Manager.cnx.startTransaction(); 38 | runMainLoop(mainFun,logError,count-1); 39 | return; 40 | } 41 | if( logError == null ) { 42 | Manager.cnx.rollback(); 43 | #if neko 44 | neko.Lib.rethrow(e); 45 | #else 46 | throw e; 47 | #end 48 | } 49 | logError(e); // should ROLLBACK if needed 50 | } 51 | } 52 | 53 | public static function main( cnx, mainFun : Void -> Void, ?logError : Dynamic -> Void ) { 54 | Manager.initialize(); 55 | Manager.cnx = cnx; 56 | Manager.cnx.startTransaction(); 57 | runMainLoop(mainFun,logError,3); 58 | try { 59 | Manager.cnx.commit(); 60 | } catch( e : String ) { 61 | // sqlite can have errors on commit 62 | if( ~/Database is busy/.match(e) ) 63 | logError(e); 64 | } 65 | Manager.cnx.close(); 66 | Manager.cnx = null; 67 | Manager.cleanup(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/sys/db/Types.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2005-2016 Haxe Foundation 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | package sys.db; 23 | 24 | // basic types 25 | 26 | /** int with auto increment **/ 27 | @:noPackageRestrict 28 | typedef SId = Null 29 | 30 | /** int unsigned with auto increment **/ 31 | typedef SUId = Null 32 | 33 | /** big int with auto increment **/ 34 | typedef SBigId = Null 35 | 36 | typedef SInt = Null 37 | 38 | typedef SUInt = Null 39 | 40 | typedef SBigInt = Null 41 | 42 | /** single precision float **/ 43 | typedef SSingle = Null 44 | 45 | /** double precision float **/ 46 | typedef SFloat = Null 47 | 48 | /** use `tinyint(1)` to distinguish with int **/ 49 | typedef SBool = Null 50 | 51 | /** same as `varchar(n)` **/ 52 | typedef SString = String 53 | 54 | /** date only, use `SDateTime` for date+time **/ 55 | typedef SDate = Date 56 | 57 | /** mysql DateTime **/ 58 | typedef SDateTime = Date 59 | 60 | /** mysql Timestamp **/ 61 | typedef STimeStamp = Date 62 | 63 | /** TinyText (up to 255 bytes) **/ 64 | typedef STinyText = String 65 | 66 | /** Text (up to 64KB) **/ 67 | typedef SSmallText = String 68 | 69 | /** MediumText (up to 24MB) **/ 70 | typedef SText = String 71 | 72 | /** Blob type (up to 64KB) **/ 73 | typedef SSmallBinary = haxe.io.Bytes 74 | 75 | /** LongBlob type (up to 4GB) **/ 76 | typedef SLongBinary = haxe.io.Bytes 77 | 78 | /** MediumBlob type (up to 24MB) **/ 79 | typedef SBinary = haxe.io.Bytes 80 | 81 | /** same as binary(n) **/ 82 | typedef SBytes = haxe.io.Bytes 83 | 84 | /** one byte signed `-128...127` **/ 85 | typedef STinyInt = Null 86 | 87 | /** two bytes signed `-32768...32767` **/ 88 | typedef SSmallInt = Null 89 | 90 | /** three bytes signed `-8388608...8388607` **/ 91 | typedef SMediumInt = Null 92 | 93 | /** one byte `0...255` **/ 94 | typedef STinyUInt = Null 95 | 96 | /** two bytes `0...65535` **/ 97 | typedef SSmallUInt = Null 98 | 99 | /** three bytes `0...16777215` **/ 100 | typedef SMediumUInt = Null 101 | 102 | // extra 103 | 104 | /** specify that this field is nullable **/ 105 | typedef SNull = Null 106 | 107 | /** specify that the integer use custom encoding **/ 108 | typedef SEncoded = Null 109 | 110 | /** Haxe Serialized string **/ 111 | typedef SSerialized = String 112 | 113 | /** native neko serialized bytes **/ 114 | typedef SNekoSerialized = haxe.io.Bytes 115 | 116 | /** a set of bitflags of different enum values **/ 117 | typedef SFlags = Null> 118 | 119 | /** same as `SFlags` but will adapt the storage size to the number of flags **/ 120 | typedef SSmallFlags = SFlags; 121 | 122 | /** allow to store any value in serialized form **/ 123 | typedef SData = Null 124 | 125 | /** allow to store an enum value that does not have parameters as a simple int **/ 126 | typedef SEnum = Null 127 | 128 | -------------------------------------------------------------------------------- /test-common.hxml: -------------------------------------------------------------------------------- 1 | -lib record-macros 2 | -cp test 3 | -lib utest 4 | -main Main 5 | -dce full 6 | -D analyzer-optimize 7 | -------------------------------------------------------------------------------- /test-cpp.hxml: -------------------------------------------------------------------------------- 1 | test-common.hxml 2 | -cpp build/cpp 3 | -debug 4 | -------------------------------------------------------------------------------- /test-hl.hxml: -------------------------------------------------------------------------------- 1 | test-common.hxml 2 | -hl build/test.hl 3 | -------------------------------------------------------------------------------- /test-neko.hxml: -------------------------------------------------------------------------------- 1 | test-common.hxml 2 | -neko build/test.n 3 | -------------------------------------------------------------------------------- /test-php.hxml: -------------------------------------------------------------------------------- 1 | test-common.hxml 2 | -php build/php 3 | -------------------------------------------------------------------------------- /test/CommonDatabaseTest.hx: -------------------------------------------------------------------------------- 1 | import MySpodClass; 2 | import haxe.EnumFlags; 3 | import haxe.io.Bytes; 4 | import utest.Assert; 5 | import sys.db.*; 6 | import sys.db.Types; 7 | 8 | using Lambda; 9 | 10 | class CommonDatabaseTest extends utest.Test { 11 | function connect() 12 | { 13 | throw "Not implemented"; 14 | } 15 | 16 | static var testClasses:Array<{ manager:Manager }> = [ 17 | MySpodClass, 18 | OtherSpodClass, 19 | NullableSpodClass, 20 | ClassWithStringId, 21 | ClassWithStringIdRef, 22 | IssueC3828, 23 | Issue6041Table, 24 | Issue19SpodClass, 25 | PublicKeyClass, 26 | PublicKeyRelationClass 27 | ]; 28 | 29 | public function setup() 30 | { 31 | Manager.initialize(); 32 | connect(); 33 | for (cls in testClasses) { 34 | var quoteField = @:privateAccess cls.manager.quoteField; 35 | var name = cls.manager.dbInfos().name; 36 | Manager.cnx.request('DROP TABLE IF EXISTS ${quoteField(name)}'); 37 | TableCreate.create(cls.manager); 38 | } 39 | Manager.cleanup(); 40 | } 41 | 42 | public function teardown() 43 | { 44 | Manager.cnx.close(); 45 | } 46 | 47 | function assertIsInstanceOf(v:Dynamic, t:Dynamic, ?pos:haxe.PosInfos) { 48 | Assert.isTrue(Std.is(v, t), pos); 49 | } 50 | 51 | function getDefaultClass() 52 | { 53 | var scls = new MySpodClass(); 54 | scls.int = 1; 55 | scls.double = 2.0; 56 | scls.boolean = true; 57 | scls.string = "some string"; 58 | scls.date = new Date(2012, 7, 30, 0, 0, 0); 59 | scls.abstractType = "other string"; 60 | 61 | var bytes = Bytes.ofString("\x01\n\r'\x02"); 62 | scls.binary = bytes; 63 | scls.enumFlags = EnumFlags.ofInt(0); 64 | scls.enumFlags.set(FirstValue); 65 | scls.enumFlags.set(ThirdValue); 66 | scls.bytes = Bytes.ofString("\000a"); 67 | 68 | scls.data = [new ComplexClass( { name:"test", array:["this", "is", "a", "test"] } )]; 69 | scls.anEnum = SecondValue; 70 | 71 | return scls; 72 | } 73 | 74 | function getDefaultNull() { 75 | var scls = new NullableSpodClass(); 76 | scls.int = 1; 77 | scls.double = 2.0; 78 | scls.boolean = true; 79 | scls.string = "some string"; 80 | scls.date = new Date(2012, 7, 30, 0, 0, 0); 81 | scls.abstractType = "other string"; 82 | 83 | var bytes = Bytes.ofString("\x01\n\r'\x02"); 84 | scls.binary = bytes; 85 | scls.enumFlags = EnumFlags.ofInt(0); 86 | scls.enumFlags.set(FirstValue); 87 | scls.enumFlags.set(ThirdValue); 88 | 89 | scls.data = [new ComplexClass( { name:"test", array:["this", "is", "a", "test"] } )]; 90 | scls.anEnum = SecondValue; 91 | return scls; 92 | } 93 | 94 | public function testNull() { 95 | var n1 = getDefaultNull(); 96 | n1.insert(); 97 | var n2 = new NullableSpodClass(); 98 | n2.insert(); 99 | var id = n2.theId; 100 | 101 | n1 = null; n2 = null; 102 | Manager.cleanup(); 103 | 104 | var nullVal = getNull(); 105 | inline function checkReq(lst:List, ?nres=1, ?pos:haxe.PosInfos) { 106 | Assert.equals(nres, lst.length, null, pos); 107 | if (lst.length == 1) { 108 | Assert.equals(id, lst.first().theId, null, pos); 109 | } 110 | } 111 | 112 | checkReq(NullableSpodClass.manager.search($relationNullable == null), 2); 113 | checkReq(NullableSpodClass.manager.search($data == null)); 114 | checkReq(NullableSpodClass.manager.search($anEnum == null)); 115 | 116 | checkReq(NullableSpodClass.manager.search($int == null)); 117 | checkReq(NullableSpodClass.manager.search($double == null)); 118 | checkReq(NullableSpodClass.manager.search($boolean == null)); 119 | checkReq(NullableSpodClass.manager.search($string == null)); 120 | checkReq(NullableSpodClass.manager.search($date == null)); 121 | checkReq(NullableSpodClass.manager.search($binary == null)); 122 | checkReq(NullableSpodClass.manager.search($abstractType == null)); 123 | 124 | checkReq(NullableSpodClass.manager.search($enumFlags == null)); 125 | 126 | 127 | var relationNullable:Null = getNull(); 128 | checkReq(NullableSpodClass.manager.search($relationNullable == relationNullable), 2); 129 | var data:Null = getNull(); 130 | checkReq(NullableSpodClass.manager.search($data == data)); 131 | var anEnum:Null> = getNull(); 132 | checkReq(NullableSpodClass.manager.search($anEnum == anEnum)); 133 | 134 | var int:Null = getNull(); 135 | checkReq(NullableSpodClass.manager.search($int == int)); 136 | var double:Null = getNull(); 137 | checkReq(NullableSpodClass.manager.search($double == double)); 138 | var boolean:Null = getNull(); 139 | checkReq(NullableSpodClass.manager.search($boolean == boolean)); 140 | var string:SNull> = getNull(); 141 | checkReq(NullableSpodClass.manager.search($string == string)); 142 | var date:SNull = getNull(); 143 | checkReq(NullableSpodClass.manager.search($date == date)); 144 | var binary:SNull = getNull(); 145 | checkReq(NullableSpodClass.manager.search($binary == binary)); 146 | var abstractType:SNull = getNull(); 147 | checkReq(NullableSpodClass.manager.search($abstractType == abstractType)); 148 | } 149 | 150 | private function getNull():Null { 151 | return null; 152 | } 153 | 154 | public function testIssue3828() 155 | { 156 | var u1 = new IssueC3828(); 157 | u1.insert(); 158 | var u2 = new IssueC3828(); 159 | u2.refUser = u1; 160 | u2.insert(); 161 | var u1id = u1.id, u2id = u2.id; 162 | u1 = null; u2 = null; 163 | Manager.cleanup(); 164 | 165 | var u1 = IssueC3828.manager.get(u1id); 166 | var u2 = IssueC3828.manager.search($refUser == u1).first(); 167 | Assert.equals(u1id, u1.id); 168 | Assert.equals(u2id, u2.id); 169 | } 170 | 171 | public function testIssue6041() 172 | { 173 | var item = new Issue6041Table(); 174 | item.insert(); 175 | var result = Manager.cnx.request('SELECT * FROM Issue6041Table LIMIT 1'); 176 | var amount = 1; 177 | for(row in result) { 178 | Assert.isFalse(--amount < 0, "Invalid amount of rows in result"); 179 | } 180 | Assert.equals(0, amount); 181 | } 182 | 183 | public function testStringIdRel() 184 | { 185 | var s = new ClassWithStringId(); 186 | s.name = "first"; 187 | s.field = 1; 188 | s.insert(); 189 | var v1 = new ClassWithStringIdRef(); 190 | v1.ref = s; 191 | v1.insert(); 192 | var v2 = new ClassWithStringIdRef(); 193 | v2.ref = s; 194 | v2.insert(); 195 | 196 | s = new ClassWithStringId(); 197 | s.name = "second"; 198 | s.field = 2; 199 | s.insert(); 200 | v1 = new ClassWithStringIdRef(); 201 | v1.ref = s; 202 | v1.insert(); 203 | s = null; v1 = null; v2 = null; 204 | Manager.cleanup(); 205 | 206 | var first = ClassWithStringId.manager.search($name == "first"); 207 | Assert.equals(1, first.length); 208 | var first = first.first(); 209 | Assert.equals(1, first.field); 210 | var frel = ClassWithStringIdRef.manager.search($ref == first); 211 | Assert.equals(2, frel.length); 212 | for (rel in frel) 213 | Assert.equals(first, rel.ref); 214 | var frel2 = ClassWithStringIdRef.manager.search($ref_id == "first"); 215 | Assert.equals(2, frel2.length); 216 | for (rel in frel2) 217 | Assert.equals(first, rel.ref); 218 | 219 | var second = ClassWithStringId.manager.search($name == "second"); 220 | Assert.equals(1, second.length); 221 | var second = second.first(); 222 | Assert.equals(2, second.field); 223 | var srel = ClassWithStringIdRef.manager.search($ref == second); 224 | Assert.equals(1, srel.length); 225 | for (rel in srel) 226 | Assert.equals(second, rel.ref); 227 | 228 | Assert.equals(-1, frel.array().indexOf(srel.first())); 229 | } 230 | 231 | public function testEnum() 232 | { 233 | var c1 = new OtherSpodClass("first spod"); 234 | c1.insert(); 235 | var c2 = new OtherSpodClass("second spod"); 236 | c2.insert(); 237 | 238 | var scls = getDefaultClass(); 239 | var scls1 = scls; 240 | scls.relation = c1; 241 | scls.insert(); 242 | var id1 = scls.theId; 243 | scls = getDefaultClass(); 244 | scls.relation = c1; 245 | scls.insert(); 246 | 247 | scls1.next = scls; 248 | scls1.update(); 249 | 250 | var id2 = scls.theId; 251 | scls = getDefaultClass(); 252 | scls.relation = c1; 253 | scls.next = scls1; 254 | scls.anEnum = FirstValue; 255 | scls.insert(); 256 | var id3 = scls.theId; 257 | scls = null; 258 | 259 | Manager.cleanup(); 260 | var r1s = [ for (c in MySpodClass.manager.search($anEnum == SecondValue,{orderBy:theId})) c.theId ]; 261 | Assert.same(r1s, [id1, id2]); 262 | var r2s = MySpodClass.manager.search($anEnum == FirstValue); 263 | Assert.equals(1, r2s.length); 264 | Assert.equals(id3, r2s.first().theId); 265 | Assert.equals(id1, r2s.first().next.theId); 266 | Assert.equals(id2, r2s.first().next.next.theId); 267 | 268 | var fv = getSecond(); 269 | var r1s = [ for (c in MySpodClass.manager.search($anEnum == fv,{orderBy:theId})) c.theId ]; 270 | Assert.same(r1s, [id1, id2]); 271 | var r2s = MySpodClass.manager.search($anEnum == getFirst()); 272 | Assert.equals(1, r2s.length); 273 | Assert.equals(id3, r2s.first().theId); 274 | 275 | var ids = [id1,id2,id3]; 276 | var s = [ for (c in MySpodClass.manager.search( $anEnum == SecondValue || ($theId in ids) )) c.theId ]; 277 | s.sort(Reflect.compare); 278 | Assert.same(s, [id1, id2, id3]); 279 | } 280 | 281 | public function getFirst() 282 | { 283 | return FirstValue; 284 | } 285 | 286 | public function getSecond() 287 | { 288 | return SecondValue; 289 | } 290 | 291 | public function testUpdate() 292 | { 293 | var c1 = new OtherSpodClass("first spod"); 294 | c1.insert(); 295 | var c2 = new OtherSpodClass("second spod"); 296 | c2.insert(); 297 | var scls = getDefaultClass(); 298 | scls.relation = c1; 299 | scls.relationNullable = c2; 300 | scls.insert(); 301 | 302 | var id = scls.theId; 303 | 304 | //if no change made, update should return nothing 305 | Assert.isNull(untyped MySpodClass.manager.getUpdateStatement(scls)); 306 | Manager.cleanup(); 307 | scls = MySpodClass.manager.get(id); 308 | Assert.isNull(untyped MySpodClass.manager.getUpdateStatement(scls)); 309 | scls.delete(); 310 | 311 | //try now with null SData and null relation 312 | var scls = new NullableSpodClass(); 313 | scls.insert(); 314 | 315 | var id = scls.theId; 316 | 317 | //if no change made, update should return nothing 318 | Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); 319 | Manager.cleanup(); 320 | scls = NullableSpodClass.manager.get(id); 321 | Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); 322 | Assert.isNull(scls.data); 323 | Assert.isNull(scls.relationNullable); 324 | Assert.isNull(scls.abstractType); 325 | Assert.isNull(scls.anEnum); 326 | scls.delete(); 327 | 328 | //same thing with explicit null set 329 | var scls = new NullableSpodClass(); 330 | scls.data = null; 331 | scls.relationNullable = null; 332 | scls.abstractType = null; 333 | scls.anEnum = null; 334 | scls.insert(); 335 | 336 | var id = scls.theId; 337 | 338 | //if no change made, update should return nothing 339 | Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); 340 | Manager.cleanup(); 341 | scls = NullableSpodClass.manager.get(id); 342 | Assert.isNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); 343 | Assert.isNull(scls.data); 344 | Assert.isNull(scls.relationNullable); 345 | Assert.isNull(scls.abstractType); 346 | Assert.isNull(scls.anEnum); 347 | Manager.cleanup(); 348 | 349 | scls = new NullableSpodClass(); 350 | scls.theId = id; 351 | Assert.notNull(untyped NullableSpodClass.manager.getUpdateStatement(scls)); 352 | } 353 | 354 | public function testSpodTypes() 355 | { 356 | var c1 = new OtherSpodClass("first spod"); 357 | c1.insert(); 358 | var c2 = new OtherSpodClass("second spod"); 359 | c2.insert(); 360 | 361 | var scls = getDefaultClass(); 362 | 363 | scls.relation = c1; 364 | scls.relationNullable = c2; 365 | scls.insert(); 366 | 367 | //after inserting, id must be filled 368 | Assert.notEquals(0, scls.theId); 369 | Assert.notNull(scls.theId); 370 | var theid = scls.theId; 371 | 372 | c1 = c2 = null; 373 | Manager.cleanup(); 374 | 375 | var cls1 = MySpodClass.manager.get(theid); 376 | Assert.notNull(cls1); 377 | //after Manager.cleanup(), the instances should be different 378 | Assert.isFalse(cls1 == scls); 379 | scls = null; 380 | 381 | assertIsInstanceOf(cls1.int, Int); 382 | Assert.equals(1, cls1.int); 383 | assertIsInstanceOf(cls1.double, Float); 384 | Assert.equals(2.0, cls1.double); 385 | assertIsInstanceOf(cls1.boolean, Bool); 386 | Assert.isTrue(cls1.boolean); 387 | assertIsInstanceOf(cls1.string, String); 388 | Assert.equals("some string", cls1.string); 389 | assertIsInstanceOf(cls1.abstractType, String); 390 | Assert.equals("other string", cls1.abstractType.get()); 391 | Assert.notNull(cls1.date); 392 | assertIsInstanceOf(cls1.date, Date); 393 | #if (haxe_ver > 3.407) 394 | Assert.equals(new Date(2012, 7, 30, 0, 0, 0).getTime(), cls1.date.getTime()); 395 | #else 396 | // see haxe#6530 ("[cpp] fix Mysql.secondsToDate: Date.fromTime expects milliseconds") 397 | #end 398 | 399 | assertIsInstanceOf(cls1.binary, Bytes); 400 | Assert.equals(0, cls1.binary.compare(Bytes.ofString("\x01\n\r'\x02"))); 401 | Assert.isTrue(cls1.enumFlags.has(FirstValue)); 402 | Assert.isFalse(cls1.enumFlags.has(SecondValue)); 403 | Assert.isTrue(cls1.enumFlags.has(ThirdValue)); 404 | 405 | assertIsInstanceOf(cls1.data, Array); 406 | assertIsInstanceOf(cls1.data[0], ComplexClass); 407 | 408 | Assert.equals("test", cls1.data[0].val.name); 409 | Assert.equals(4, cls1.data[0].val.array.length); 410 | Assert.equals("is", cls1.data[0].val.array[1]); 411 | 412 | Assert.equals("first spod", cls1.relation.name); 413 | Assert.equals("second spod", cls1.relationNullable.name); 414 | 415 | Assert.equals(SecondValue, cls1.anEnum); 416 | assertIsInstanceOf(cls1.anEnum, SpodEnum); 417 | 418 | #if hl 419 | Assert.equals(haxe.io.Bytes.ofString("\000a").toHex(), cls1.bytes.toHex()); 420 | #else 421 | Assert.equals("\000a", cls1.bytes.toString()); 422 | #end 423 | 424 | Assert.equals(MySpodClass.manager.select($anEnum == SecondValue), cls1); 425 | 426 | //test create a new class 427 | var scls = getDefaultClass(); 428 | 429 | c1 = new OtherSpodClass("third spod"); 430 | c1.insert(); 431 | 432 | scls.relation = c1; 433 | scls.insert(); 434 | 435 | scls = cls1 = null; 436 | Manager.cleanup(); 437 | 438 | Assert.equals(2, MySpodClass.manager.all().length); 439 | var req = MySpodClass.manager.search({ relation: OtherSpodClass.manager.select({ name:"third spod"} ) }); 440 | Assert.equals(1, req.length); 441 | scls = req.first(); 442 | 443 | scls.relation.name = "Test"; 444 | scls.relation.update(); 445 | 446 | Assert.isNull(OtherSpodClass.manager.select({ name:"third spod" })); 447 | 448 | for (c in MySpodClass.manager.all()) 449 | c.delete(); 450 | for (c in OtherSpodClass.manager.all()) 451 | c.delete(); 452 | 453 | //issue #3598 454 | var inexistent = MySpodClass.manager.get(1000,false); 455 | Assert.isNull(inexistent); 456 | } 457 | 458 | public function testDateQuery() 459 | { 460 | var other1 = new OtherSpodClass("required field"); 461 | other1.insert(); 462 | 463 | var now = Date.now(); 464 | var c1 = getDefaultClass(); 465 | c1.relation = other1; 466 | c1.date = now; 467 | c1.insert(); 468 | 469 | var c2 = getDefaultClass(); 470 | c2.relation = other1; 471 | c2.date = DateTools.delta(now, DateTools.hours(1)); 472 | c2.insert(); 473 | 474 | var q = MySpodClass.manager.search($date > now); 475 | Assert.equals(1, q.length); 476 | Assert.equals(c2, q.first()); 477 | 478 | q = MySpodClass.manager.search($date == now); 479 | Assert.equals(1, q.length); 480 | Assert.equals(c1, q.first()); 481 | 482 | q = MySpodClass.manager.search($date >= now); 483 | Assert.equals(2, q.length); 484 | Assert.equals(c1, q.first()); 485 | 486 | q = MySpodClass.manager.search($date >= DateTools.delta(now, DateTools.hours(2))); 487 | Assert.equals(0, q.length); 488 | Assert.isNull(q.first()); 489 | } 490 | 491 | public function testData() 492 | { 493 | var other1 = new OtherSpodClass("required field"); 494 | other1.insert(); 495 | 496 | var c1 = getDefaultClass(); 497 | c1.relation = other1; 498 | c1.insert(); 499 | 500 | Assert.equals(1, c1.data.length); 501 | c1.data.pop(); 502 | c1.update(); 503 | 504 | Manager.cleanup(); 505 | c1 = null; 506 | 507 | c1 = MySpodClass.manager.select($relation == other1); 508 | Assert.equals(0, c1.data.length); 509 | c1.data.push(new ComplexClass({ name: "test1", array:["complex","field"] })); 510 | c1.data.push(null); 511 | Assert.equals(2, c1.data.length); 512 | c1.update(); 513 | 514 | Manager.cleanup(); 515 | c1 = null; 516 | 517 | c1 = MySpodClass.manager.select($relation == other1); 518 | Assert.equals(2, c1.data.length); 519 | Assert.equals("test1", c1.data[0].val.name); 520 | Assert.equals(2, c1.data[0].val.array.length); 521 | Assert.equals("complex", c1.data[0].val.array[0]); 522 | Assert.isNull(c1.data[1]); 523 | } 524 | 525 | public function testStringConcatenation() 526 | { 527 | var other1 = new OtherSpodClass("required field"); 528 | other1.insert(); 529 | 530 | var c1 = getDefaultClass(); 531 | c1.int = 4; 532 | c1.string = "testData" + c1.int; 533 | c1.relation = other1; 534 | c1.insert(); 535 | 536 | var q = MySpodClass.manager.select($string == "test" + "Data" + $int); 537 | Assert.notNull(q); 538 | } 539 | 540 | public function testBoolean() 541 | { 542 | var other = new OtherSpodClass("required field"); 543 | other.insert(); 544 | 545 | var c1 = getDefaultClass(); 546 | c1.boolean = false; 547 | c1.relation = other; 548 | c1.insert(); 549 | 550 | var c2 = getDefaultClass(); 551 | c2.boolean = true; 552 | c2.relation = other; 553 | c2.insert(); 554 | 555 | var mgr = MySpodClass.manager; 556 | Assert.same([c1.theId], [for (r in mgr.search($boolean == false)) r.theId]); 557 | Assert.same([c2.theId], [for (r in mgr.search($boolean == true)) r.theId]); 558 | Assert.same([c2.theId], [for (r in mgr.search($boolean)) r.theId]); 559 | Assert.same([c1.theId, c2.theId], [for (r in mgr.search(true)) r.theId]); 560 | Assert.same([c1.theId, c2.theId], [for (r in mgr.search({})) r.theId]); 561 | Assert.same([c1.theId, c2.theId], [for (r in mgr.dynamicSearch({})) r.theId]); 562 | Assert.same([], [for (r in mgr.search($theId in [])) r.theId]); 563 | } 564 | 565 | /** 566 | Check that relations are not affected by the analyzer 567 | **/ 568 | public function testIssue6() 569 | { 570 | /* 571 | The way the analyzer transforms the expression (to prevent 572 | potential side-effects) might change the context where `untyped 573 | __this__` is evaluated. 574 | 575 | See: #6 and HaxeFoundation/haxe#6048 576 | */ 577 | var parent = new MySpodClass(); 578 | parent.relation = new OtherSpodClass("i"); 579 | 580 | Assert.notNull(parent.relation); 581 | Assert.equals("i", parent.relation.name); 582 | } 583 | 584 | /** 585 | Ensure that field types using full paths can be matched 586 | **/ 587 | public function testIssue19() 588 | { 589 | var val = new Issue19SpodClass(); 590 | val.anEnum = SecondValue; 591 | val.insert(); 592 | Assert.pass(); // not failing on insert() is enough 593 | } 594 | 595 | /** 596 | Test that cache management doesn't break @:skip fields 597 | **/ 598 | public function testIssue34() 599 | { 600 | var child = new OtherSpodClass("i"); 601 | child.insert(); 602 | var main = getDefaultClass(); 603 | main.relation = child; 604 | main.insert(); 605 | Manager.cleanup(); 606 | 607 | // underlying problem 608 | child = OtherSpodClass.manager.all(false).first(); 609 | child = OtherSpodClass.manager.all(true).first(); 610 | Assert.isNull(child.ignored); 611 | 612 | // reported/real world case 613 | main = MySpodClass.manager.all().first(); 614 | Assert.notNull(main.relation); // cache child, but !lock 615 | child = OtherSpodClass.manager.all().first(); // cache and lock 616 | Assert.isNull(child.ignored); 617 | } 618 | 619 | public function testPublicForeignKey( ) 620 | { 621 | var rel = new PublicKeyRelationClass(); 622 | rel.insert(); 623 | 624 | var pk = new PublicKeyClass(); 625 | pk.relation = rel; 626 | pk.insert(); 627 | Manager.cleanup(); 628 | 629 | rel = PublicKeyRelationClass.manager.all(false).first(); 630 | Assert.notNull(rel); 631 | pk = PublicKeyClass.manager.all(false).first(); 632 | Assert.notNull(pk); 633 | Assert.notNull(pk.relation); 634 | Assert.equals(pk.relation_id, rel.id); 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /test/Main.hx: -------------------------------------------------------------------------------- 1 | class Main { 2 | static function main() { 3 | 4 | var runner = new utest.Runner(); 5 | utest.ui.Report.create(runner); 6 | 7 | var sqlitePath = ":memory:"; 8 | var mysqlParams = null; 9 | 10 | for (arg in Sys.args()) { 11 | switch arg { 12 | case dbstr if (mysqlParams == null && dbstr.indexOf("mysql://") == 0): 13 | var dbreg = ~/([^:]+):\/\/([^:]+):([^@]*?)@([^:]+)(:[0-9]+)?\/(.*?)$/; 14 | if (!dbreg.match(dbstr)) 15 | throw "Configuration requires a valid database attribute, format is : mysql://user:password@host:port/dbname"; 16 | var port = dbreg.matched(5); 17 | mysqlParams = { 18 | user:dbreg.matched(2), 19 | pass:dbreg.matched(3), 20 | host:dbreg.matched(4), 21 | port:port == null ? 3306 : Std.parseInt(port.substr(1)), 22 | database:dbreg.matched(6), 23 | socket:null 24 | }; 25 | case "--on-disk-sqlite": 26 | sqlitePath = "test.db"; 27 | case other: 28 | throw 'Unsupported command line option or parameter: $other'; 29 | } 30 | } 31 | 32 | runner.addCase(new SqliteTest(sqlitePath)); 33 | if (mysqlParams != null) 34 | runner.addCase(new MysqlTest(mysqlParams)); 35 | 36 | runner.run(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/MySpodClass.hx: -------------------------------------------------------------------------------- 1 | import sys.db.Object; 2 | import sys.db.Types; 3 | 4 | @:keep class MySpodClass extends Object 5 | { 6 | public var theId:SId; 7 | public var int:SInt; 8 | public var double:SFloat; 9 | public var boolean:SBool; 10 | public var string:SString<255>; 11 | public var date:SDateTime; 12 | public var binary:SBinary; 13 | public var abstractType:AbstractSpodTest; 14 | 15 | public var nullInt:SNull; 16 | public var enumFlags:SFlags; 17 | 18 | @:relation(rid) public var relation:OtherSpodClass; 19 | @:relation(rnid) public var relationNullable:Null; 20 | @:relation(spid) public var next:Null; 21 | 22 | public var data:SData>; 23 | public var anEnum:SEnum; 24 | public var bytes:SBytes<2>; 25 | } 26 | 27 | @:keep class NullableSpodClass extends Object 28 | { 29 | public var theId:SId; 30 | @:relation(rnid) public var relationNullable:Null; 31 | public var data:Null>>; 32 | public var anEnum:Null>; 33 | 34 | public var int:SNull; 35 | public var double:SNull; 36 | public var boolean:SNull; 37 | public var string:SNull>; 38 | public var date:SNull; 39 | public var binary:SNull; 40 | public var abstractType:SNull>; 41 | 42 | public var nullInt:SNull; 43 | public var enumFlags:SNull>; 44 | } 45 | 46 | @:keep class ComplexClass 47 | { 48 | public var val : { name:String, array:Array }; 49 | 50 | public function new(val) 51 | { 52 | this.val = val; 53 | } 54 | } 55 | 56 | @:id(theid) @:keep class OtherSpodClass extends Object 57 | { 58 | public var theid:SInt; 59 | public var name:SString<255>; 60 | 61 | @:skip public var ignored:String; 62 | 63 | public function new(name:String) 64 | { 65 | super(); 66 | this.name =name; 67 | } 68 | } 69 | 70 | @:keep enum SpodEnum 71 | { 72 | FirstValue; 73 | SecondValue; 74 | ThirdValue; 75 | } 76 | 77 | abstract AbstractSpodTest(A) from A 78 | { 79 | public function get():A 80 | { 81 | return this; 82 | } 83 | } 84 | 85 | @:id(name) 86 | @:keep class ClassWithStringId extends Object 87 | { 88 | public var name:SString<255>; 89 | public var field:SInt; 90 | } 91 | 92 | @:keep class ClassWithStringIdRef extends Object 93 | { 94 | public var id:SId; 95 | @:relation(ref_id) public var ref:ClassWithStringId; 96 | } 97 | 98 | 99 | //issue #3828 100 | @:keep @:skip class BaseIssueC3828 extends sys.db.Object { 101 | public var id : SInt; 102 | @:relation(ruid) 103 | public var refUser : SNull; 104 | } 105 | 106 | @:keep class IssueC3828 extends BaseIssueC3828 { 107 | } 108 | 109 | @:keep class Issue6041Table extends Object { 110 | public var id:SInt = 0; 111 | } 112 | 113 | // issue # 114 | class TLazyIssueFoo extends sys.db.Object { 115 | public var id:SId; 116 | @:relation(bid) public var bar:TLazyIssueBar; 117 | 118 | public function new(bar:TLazyIssueBar) 119 | { 120 | var lastFoo = TLazyIssueFoo.manager.select($bar == bar, { orderBy : -id, limit : 1 }, false); 121 | super(); 122 | } 123 | } 124 | class TLazyIssueBar extends sys.db.Object { 125 | public var id:SId; 126 | public var initialized:SString<255> = "bar"; 127 | } 128 | 129 | 130 | @:keep class Issue19SpodClass extends Object { 131 | public var id:SId; 132 | public var anEnum:sys.db.Types.SEnum; 133 | } 134 | 135 | class PublicKeyClass extends Object { 136 | public var id:SId; 137 | public var relation_id : SInt; 138 | @:relation(relation_id) public var relation:PublicKeyRelationClass; 139 | } 140 | class PublicKeyRelationClass extends Object { 141 | public var id:SId; 142 | } 143 | -------------------------------------------------------------------------------- /test/MysqlTest.hx: -------------------------------------------------------------------------------- 1 | class MysqlTest extends CommonDatabaseTest { 2 | var dbparams:{ 3 | host:String, 4 | ?port:Int, 5 | user:String, 6 | pass:String, 7 | ?socket:String, 8 | ?database:String, 9 | }; 10 | 11 | public function new(dbparams) { 12 | this.dbparams = dbparams; 13 | super(); 14 | } 15 | 16 | override function connect() { 17 | sys.db.Manager.cnx = sys.db.Mysql.connect(dbparams); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/SqliteTest.hx: -------------------------------------------------------------------------------- 1 | class SqliteTest extends CommonDatabaseTest { 2 | var dbPath:String; 3 | 4 | public function new(dbPath) { 5 | this.dbPath = dbPath; 6 | super(); 7 | } 8 | 9 | override function connect() { 10 | sys.db.Manager.cnx = sys.db.Sqlite.open(dbPath); 11 | } 12 | } 13 | --------------------------------------------------------------------------------