├── .gitignore ├── .npmignore ├── Gruntfile.coffee ├── README.md ├── benchmarks ├── assert.js ├── crud.coffee ├── escape.js ├── hg19_kgXref.txt ├── jsrel-0.2.7.js ├── order.js ├── rel.js ├── tsort.js ├── uniq.js └── unshift.js ├── bin └── install-jsrel.sh ├── lib └── jsrel.js ├── package.json ├── src ├── jsrel.coffee └── test │ └── reload.coffee └── test ├── data ├── artists.js └── genes ├── dcrud.js ├── hooks.js ├── inout.js ├── reload.js ├── schema.js └── statics.js /.gitignore: -------------------------------------------------------------------------------- 1 | benchmarks/kgxref 2 | node_modules 3 | lab 4 | test/tmp 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | lab/* 3 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | pkg: grunt.file.readJSON "package.json" 4 | coffee: 5 | compile: 6 | files: 7 | "lib/jsrel.js": "src/jsrel.coffee" 8 | "test/reload.js": "src/test/reload.coffee" 9 | 10 | grunt.loadNpmTasks "grunt-contrib-coffee" 11 | grunt.registerTask "default", ["coffee"] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSRel 2 | ========= 3 | 4 | description 5 | ------------ 6 | JavaScript synchronous RDB (Relational database) without SQL 7 | 8 | Available in modern browsers, Node.js and Titanium(NEW!). 9 | 10 | This **ISN'T** ORM, but SQL-less RDB implemented in JavaScript! 11 | 12 | 13 | Get it! 14 | ---------- 15 | ```bash 16 | $ npm install jsrel 17 | ``` 18 | 19 | or 20 | 21 | ```bash 22 | $ curl https://raw.github.com/shinout/jsrel/master/install-jsrel.sh | sh 23 | ``` 24 | 25 | 26 | API at a glance 27 | ---------------- 28 | First, define the schema 29 | 30 | ```js 31 | var JSRel = require("jsrel"); 32 | var db = JSRel.create("dbname", {schema: 33 | { user: { name : true, is_activated: "on", $uniques: "name"}, 34 | book: { title: true, price: 1, author: "user", $indexes: "title" }, 35 | }}); 36 | ``` 37 | 38 | Second, insert data 39 | 40 | ```js 41 | if (!db.loaded) { // if loaded from saved data, omits this section 42 | var u1 = db.ins('user', {name: 'shinout'}); 43 | var u2 = db.ins('user', {name: 'xxxxx', is_activated: false}); 44 | var b1 = db.ins('book', {title: 'how to jsrel', price: 10, author: u1}); 45 | var b2 = db.ins('book', {title: 'JSRel API doc', price: 20, author_id: u1.id}); 46 | } 47 | ``` 48 | 49 | Find them! 50 | 51 | ```js 52 | var users = db.find('user', {is_activated: true}); 53 | ``` 54 | 55 | Get one! 56 | 57 | ```js 58 | var shinout = db.one('user', {name: "shinout"}); 59 | ``` 60 | 61 | Greater Than, Less Equal! 62 | 63 | ```js 64 | var booksGreaterThan5 = db.find('book', { price: {gt: 5} } ); 65 | var booksLessEqual15 = db.find('book', { price: {le: 15} } ); 66 | ``` 67 | 68 | Like xxx% 69 | 70 | ```js 71 | var booksLikeJS = db.find('book', { title: {like$: "JS"} } ); 72 | ``` 73 | 74 | Join! 75 | 76 | ```js 77 | var usersJoinBooks = db.find('user', {is_activated: true}, {join: "book"}); 78 | ``` 79 | 80 | OrderBy! Offset! Limit! 81 | 82 | ```js 83 | var users = db.find('user', null, {order: "name", limit : 10, offset : 3} ); 84 | ``` 85 | 86 | 87 | Perpetuation 88 | 89 | ```js 90 | db.save(); 91 | ``` 92 | 93 | Export / Import 94 | 95 | ```js 96 | var str = db.export(); 97 | var newDB = JSRel.import("newID", str); 98 | ``` 99 | 100 | dump as SQL! 101 | 102 | ```js 103 | var sql = db.toSQL(); 104 | ``` 105 | 106 | suitable applications 107 | --------------------- 108 | 109 | - rich client applications 110 | - tiny serverside applications 111 | - client caching 112 | - mock DB 113 | 114 | NOT suitable for applications which require scalability. 115 | 116 | 117 | motivation 118 | ------------- 119 | Thinking about the separation of the Model layer. 120 | 121 | If we connect to DB asynchronously, we must handle lots of callbacks in a model method. 122 | 123 | ```js 124 | model.getUserBooks = function(name, callback) { 125 | db.find("user", {name: name}, function(err, users) { 126 | db.find("book", {user_id: users[0].id}, callback); 127 | }); 128 | }; 129 | ``` 130 | 131 | If we access to DB synchoronously, we can easily write human-readable model APIs. 132 | 133 | ```js 134 | model.getUserBooks = function(name) { 135 | var user = db.find("user", {name: "xxyy"})[0]; 136 | return db.find("book", {user_id: user.id}); 137 | }; 138 | ``` 139 | 140 | Also, synchoronous codes have an advantage of error handling. 141 | 142 | ###for those who dislike Blocking APIs### 143 | 144 | Why not making it standalone using WebWorker (browsers) or child_process.fork() (Node.js)? 145 | Then the whole calculation process doesn't affect the main event loop and we can get the result asynchronously. 146 | 147 | I prepared another JavaScript library for this purpose. 148 | 149 | [standalone](https://github.com/shinout/standalone). 150 | 151 | Then, we can access model methods like 152 | 153 | ```js 154 | model.getUserBooks("user01", function(err, result) { 155 | }) 156 | ``` 157 | 158 | by defining 159 | 160 | ```js 161 | model.getUserBooks = function(name) { 162 | var user = db.find("user", {name: "xxyy"})[0]; 163 | if (!user) return []; 164 | return db.find("book", {user_id: user.id}); 165 | }; 166 | ``` 167 | 168 | That is, try/catch and asynchronous APIs are automatically created via [standalone](https://github.com/shinout/standalone). 169 | 170 | See **make it standalone** for detailed usage. 171 | 172 | 173 | installation 174 | ------------- 175 | 176 | ```bash 177 | $ npm install jsrel 178 | ``` 179 | 180 | for development in Titanium or web browsers, 181 | 182 | ```bash 183 | $ curl https://raw.github.com/shinout/jsrel/master/install-jsrel.sh | sh 184 | ``` 185 | 186 | in browsers, 187 | 188 | ```html 189 | 190 | 191 | ``` 192 | 193 | in Node.js or Titanium, 194 | 195 | ```js 196 | var JSRel = require('jsrel'); 197 | ``` 198 | 199 | is the way to load the library. 200 | 201 | In browsers, the variable "JSRel" is set to global. 202 | 203 | In Web Worker, 204 | 205 | ```js 206 | importScripts('/pathto/SortedList.js', '/pathto/jsrel.js'); 207 | ``` 208 | 209 | See also **make it standalone**. 210 | 211 | dependencies 212 | ------------- 213 | JSRel internally uses **[SortedList](https://github.com/shinout/SortedList)** 214 | When installed with npm, it is automatically packed to node_modules/sortedlist 215 | Otherwise, it is recommended to run the following command to prepare jsrel and sortedlist. 216 | 217 | ```bash 218 | $ curl https://raw.github.com/shinout/jsrel/master/install-jsrel.sh | sh 219 | ``` 220 | 221 | In Titanium, you have to set jsrel.js and SortedList.js at the top of Resources directory. 222 | 223 | 224 | JSRel API documentation 225 | ------------------------- 226 | 227 | **JSRel** 228 | 229 | - JSRel.use(uniqId, options) 230 | - JSRel.create(uniqId, options) 231 | - JSRel.createIfNotExists(uniqId, options) 232 | - JSRel.import(uniqId, data_str, options) 233 | - JSRel.$import(uniqId, data_str, options) 234 | - JSRel.uniqIds 235 | - JSRel.isNode 236 | - JSRel.isBrowser 237 | - JSRel.isTitanium 238 | 239 | **instance of JSRel (jsrel)** 240 | 241 | - jsrel.table(tableName) 242 | - jsrel.save() 243 | - jsrel.export(noCompress) 244 | - jsrel.$export(noCompress) 245 | - jsrel.on(eventName, func, options) 246 | - jsrel.off(eventName, func) 247 | - jsrel.toSQL(options) 248 | - jsrel.origin() 249 | - jsrel.drop() 250 | - jsrel.id 251 | - jsrel.name 252 | - jsrel.tables 253 | - jsrel.schema 254 | - jsrel.loaded 255 | - jsrel.created 256 | 257 | 258 | **instance of JSRel Table (table)** 259 | 260 | - table.columns 261 | - table.ins(obj) 262 | - table.upd(obj, options) 263 | - table.find(query, options) 264 | - table.one(id) 265 | - table.one(query, options) 266 | - table.del(id) 267 | - table.del(query) 268 | 269 | 270 | **shortcut** 271 | 272 | - jsrel.ins(tableName, ...) 273 | - jsrel.upd(tableName, ...) 274 | - jsrel.find(tableName, ...) 275 | - jsrel.one(tableName, ...) 276 | - jsrel.del(tableName, ...) 277 | 278 | 279 | ### JSRel.use(uniqId, options) ### 280 | Creates instance if not exist. 281 | Gets previously created instance if already exists. 282 | 283 | **uniqId** is the identifier of the instance, used for storing the data to external system (file system, localStorage and so on). 284 | **options** is as follows. 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 |
keytyperequired?descriptionexample
storagestringnotype of external storages. oneof "mock", file" "local" "session"
296 | When running in Node.js or in Titanium, "file" is set by default.
297 | uniqId is the path name to save the data to. 298 | When running in browsers, "local" is set by default.
299 | local means "localStorage", session means "sessionStorage". 300 | When running in Web Worker, "mock" is set and no other options can be selected.
301 | "mock" saves nothing. This is limitation of Web Worker which cannot access to Web Storages. 302 | In this case, exporting the data to the main thread, we can manually handle and store the data.
303 |
"file"
schemaobjectrequiredDB schema(see SCHEMA JSON)
resetbooleanno (default false)if true, reset db with the given schema.true
namestringno (default : the same as uniqId)the name of the dbappname_test
autosavebooleanno (default false)whether to auto-saving or nottrue
331 | 332 | #### SCHEMA JSON #### 333 | 334 | ```js 335 | { 336 | tableName1: tableDescription, 337 | tableName2: { 338 | columnName1 : columnDescription, 339 | columnName2 : columnDescription 340 | } 341 | } 342 | ``` 343 | 344 | **table description** 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 363 | 364 | 365 | 366 | 367 | 368 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 |
keytypedescriptionexample
(columnName)columnDescriptioncolumn to set.
355 | name limitation
356 | Cannot set [id, ins_at, upd_at] as they are already used by default.
357 | Cannot set [$indexes, $uniques, $classes] as they make conflict in schema description.
358 | Cannot set [str, num, bool, on, off] as they make conflict in column description.
359 | Cannot set [join, order, limit, offset, as, where, select, explain] as they make conflict in search options.
360 | Cannot include "," or "." as it is used in indexing or searching.
361 | Cannot set (RelatedTableName)_id as it is automatically set.
362 |
age: "num"
$indexesArraylist of indexes. child arrays are lists of columns to make an index.
369 | If string given, converted as array with the value
370 |
[["name"], ["firstName", "lastName"]]
$uniquesArray(the same as $indexes, but this means unique index)[["name", "pass"]]
$classesArray(the same as $indexes, but this means classified index)"type_num"
384 | 385 | **column description** 386 | 387 | 388 | 389 | 390 | 391 | 394 | 395 | 396 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 439 | 440 | 441 | 443 | 444 | 445 | 447 | 448 |
exampledescription
{type: "str"}type is string. 392 | type must be one of ["str", "num", "bool", (columnName)] 393 |
{type: "str", required: false}type is string, and if not given, null is set. 397 | required option is false by default 398 |
{type: "bool", _default: true}type is boolean, and if not given, true is set.
{type: "num", required: true}type is number, and if not given, an exception is thrown.
"str"type is string, and not required.
"num"type is number, and not required.
"bool"type is boolean, and not required.
truetype is string, and required.
falsetype is string, and not required.
1type is number, and required.
0type is number, and not required.
"on"type is boolean, and default value is true.
"off"type is boolean, and default value is false.
{type: tableName}type is the instance of a record in tableName.
435 | the column columnName_id is automatically created.
436 | We can set columnName_id instead of columnName in insertion and updating.
437 | This column is required unless you set required: false. 438 |
{type: tableName, required: false}type is the instance of a record in tableName and not required.
442 |
tableNametype is the instance of a record in tableName and required.
446 |
449 | 450 | ### JSRel.create(uniqId, options) ### 451 | Creates instance if not exist, like **JSRel.use**. 452 | Throws an error if already exists, unlike **JSRel.use**. 453 | Arguments are the same as JSRel.use except options.reset, which is invalid in JSRel.create() 454 | 455 | ### JSRel.createIfNotExists(uniqId, options) ### 456 | Creates instance if not exist. 457 | Gets previously created instance if already exists. 458 | **options** is optional when loading an existing database, and required when creating a new database. 459 | Actually, this is the alias for JSRel.use(uniqId, options) 460 | 461 | ### JSRel.import(uniqId, data_str, options) ### 462 | Imports **data_str** and creates a new instance with **uniqId**. 463 | **data_str** must be a stringified JSON generated by **jsrel.export()**. 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 498 | 499 | 500 |
keytyperequired?descriptionexample
forcebooleanno (default : false) if true, overrides already-existing database of the same uniqId. 477 | otherwise throws an error.true
namestringnothe name of the db. If undefined, the imported name is used.appname_2
autosavebooleannowhether to auto-saving or not. If undefineed, the imported autosave preference is used.true
storagestringnotype of external storages. see options of JSRel.use(). 496 | If undefined, the imported storage preference is used. 497 | "file"
501 | 502 | Returns instance of JSRel. 503 | 504 | ### JSRel.$import(uniqId, data_str, options) ### 505 | Alias for JSRel.import(). 506 | As "import" is a reserved word in JavaScript, we first named this function "$import". 507 | However, CoffeeScript enables us to use reserved words, then we can also use **JSRel.import** as the alias. 508 | 509 | 510 | ### JSRel.isNode ### 511 | (ReadOnly boolean) if Node.js, true. 512 | 513 | 514 | ### JSRel.isBrowser ### 515 | (ReadOnly boolean) if the executing environment has "localStorage" and "sessionStorage" in global scope, true. 516 | 517 | 518 | ### JSRel.isTitanium ### 519 | (ReadOnly boolean) if Titanium, true. 520 | 521 | instanceof JSRel (shown as jsrel) 522 | ------ 523 | ### jsrel.table(tableName) ### 524 | Returns a table object whose name is **tableName** (registered from the schema). 525 | If absent, throws an exception. 526 | 527 | 528 | ### jsrel.save() ### 529 | Saves current data to the storage. 530 | Returns **jsrel** 531 | 532 | ### jsrel.export(noCompress) ### 533 | 534 | Exports current data as the format above. 535 | Returns data. 536 | If **noCompress** is given, it returns uncompressed data. 537 | 538 | ### jsrel.$export(noCompress) ### 539 | Alias for jsrel.export() as "export" is a reserved word in JavaScript. 540 | In CoffeeScript, jsrel.export() can be safely called. 541 | 542 | 543 | ### jsrel.on(eventName, func, options) ### 544 | 545 | Registers hook functions. 546 | **eventName** is the name of the event to bind the function **func**. 547 | 548 | #### events #### 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 562 | 563 | 564 | 565 | 566 | 571 | 572 | 573 | 574 | 575 | 583 | 584 | 585 | 586 | 587 | 594 | 595 | 596 | 597 | 598 | 604 | 605 | 606 | 607 | 608 | 613 | 614 | 615 | 616 | 617 | 622 | 623 | 624 | 625 | 626 | 631 | 632 | 633 |
event nameemitted whenarguments to be passed
insdata are inserted 557 |
    558 |
  • **tableName** : table name 559 |
  • **insObj** : inserted object (with id) 560 |
561 |
ins:{tablename}data are inserted into {tablename} 567 |
    568 |
  • **insObj** : inserted object (with id) 569 |
570 |
upddata are updated 576 |
    577 |
  • **tableName** : table name 578 |
  • **updObj** : updated object 579 |
  • **oldObj** : object before updating 580 |
  • **updColumns** :updated columns (Array) 581 |
582 |
upd:{tablename}data are updated in {tablename} 588 |
    589 |
  • **updObj** : updated object 590 |
  • **oldObj** : object before updating 591 |
  • **updColumns** :updated columns (Array) 592 |
593 |
deldata are deleted 599 |
    600 |
  • **tableName** : table name 601 |
  • **delObj** : deleted object 602 |
603 |
del:{tablename}data are deleted in {tablename} 609 |
    610 |
  • **delObj** : deleted object 611 |
612 |
save:startat the start of jsrel.save() 618 |
    619 |
  • **origin** : result of db.origin() 620 |
621 |
save:endat the end of jsrel.save() 627 |
    628 |
  • **data** : saved data 629 |
630 |
634 | 635 | 636 | #### options #### 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 |
option nametypedescriptiondefault
unshiftbooleanregisters a function to the top of the listfalse
649 | 650 | 651 | 652 | ### jsrel.off(eventName, func) ### 653 | Unregister hook functions registered in **eventName**. 654 | If a function **func** is registered in **eventName** hooks, it is removed. 655 | If **func** is null, all functions registered in **eventName** is removed. 656 | 657 | 658 | 659 | ### jsrel.toSQL(options) ### 660 | Gets SQL string from the current schema and data. 661 | 662 | **options** 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 |
option nametypedescriptiondefaultexample
noschemabooleanif true, schema SQLs (create statements) are not generated.nulltrue
dbboolean or stringif true, create database whose name is id of the db, if string given, the value is set as database's name. 681 | if not set, database creation (CREATE DATABASE xxxx) does not occur. 682 | nulltrue
nodropbooleanif true, drop statements are not generated.nulltrue
nodatabooleanif true, data SQLs (insert statements) are not generated.nulltrue
typestringtype of RDBs. Currently, "mysql" is only tested."mysql""mysql"
enginestringMySQL engine (only enabled when options.type is "mysql")"InnoDB""MyISAM"
rails (unstable)booleanif true, rails-like date format (created_at, inserted_at) is output.nulltrue
722 | 723 | 724 | ### jsrel.origin() ### 725 | Gets the last savedata. 726 | 727 | Unless jsrel.save() has been called at least once, null is returned. 728 | 729 | ```js 730 | var savedata = jsrel.origin(); 731 | var newdb = JSRel.import("new_db", savedata); 732 | ``` 733 | 734 | 735 | ### jsrel.drop(tableName1, tableName2, ...) ### 736 | Drops given tables. 737 | If dependencies exist, jsrel follows the following rules. 738 | 739 | 1. throw an error if the given table group contains another reference table 740 | 2. set all the ids of referred columns to null 741 | 742 | ### jsrel.id ### 743 | (ReadOnly) gets id 744 | 745 | 746 | ### jsrel.name ### 747 | (ReadOnly) gets name 748 | 749 | 750 | ### jsrel.tables ### 751 | (ReadOnly) gets list of registered tables 752 | 753 | 754 | ```js 755 | [table1, table2, ...] 756 | ``` 757 | 758 | 759 | ### jsrel.schema ### 760 | (ReadOnly) gets a canonical schema of the database, the same format as schema passed to JSRel.use 761 | 762 | Be careful that this property is dynamically created for every access. 763 | 764 | ```js 765 | var schema = db.schema; // created dynamically 766 | var schema2 = db.schema; // created dynamically 767 | schema === schema2 // false, but deeply equal 768 | 769 | var db2 = JSRel.use("db2", {schema: schema}); // the same structure as db 770 | ``` 771 | 772 | ### jsrel.loaded ### 773 | (ReadOnly) boolean: true if loaded or imported from stored data, false otherwise. 774 | 775 | ```js 776 | db = JSRel.use("/path/to/file", {schema: {tbl1: {name:true}, tbl2: {n: 1}}}); 777 | 778 | // if not loaded, then inputs initialized data 779 | if (!db.loaded) { 780 | db.ins("tbl1", {name: "shinout"}); 781 | db.ins("tbl1", {name: "nishiko"}); 782 | db.ins("tbl2", {n: 37}); 783 | db.ins("tbl2", {n: -21}); 784 | } 785 | ``` 786 | ### jsrel.created ### 787 | (ReadOnly) boolean: true if created, false otherwise. 788 | 789 | jsrel.created === !jsrel.loaded 790 | 791 | ```js 792 | db = JSRel.use("/path/to/file", {schema: {tbl1: {name:true}, tbl2: {n: 1}}}); 793 | 794 | // if created, then inputs initialized data 795 | if (db.created) { 796 | db.ins("tbl1", {name: "shinout"}); 797 | db.ins("tbl1", {name: "nishiko"}); 798 | db.ins("tbl2", {n: 37}); 799 | db.ins("tbl2", {n: -21}); 800 | } 801 | ``` 802 | 803 | 804 | 805 | 806 | 807 | instanceof JSRel.Table (shown as table) 808 | ------ 809 | ### table.columns ### 810 | (ReadOnly) gets registered columns in the table 811 | 812 | [column1, column2, ...] 813 | 814 | ### table.ins(obj) ### 815 | Registers a new record. 816 | **obj** must be compatible with columns of the table. 817 | Otherwise it throws an exception. 818 | Returns an instance of the record. 819 | It is NOT the same as the given argument, as the new object contains "id". 820 | 821 | Before insertion, Type checking is performed. 822 | JSRel tries to cast the data. 823 | 824 | 825 | #### record object #### 826 | Record objects have all columns registered in the table. 827 | 828 | In addition, they have **id**, **ins_at**, **upd_at** in their key. 829 | These are all automatically set. 830 | 831 | **ins_at** and **upd_at** are timestamp values and cannot be inserted. 832 | 833 | **id** is auto-incremented unique integer. 834 | 835 | We can specify **id** in insertion. 836 | 837 | ```js 838 | table.ins({id: 11, name: "iPhone"}); 839 | ``` 840 | 841 | When the table already has the same id, an exception is thrown. 842 | 843 | 844 | #### relation handling in insertion #### 845 | OK, let's think upon the following schema. 846 | 847 | ```js 848 | var schema = { user: { 849 | nickName : true, 850 | fitstName: false, 851 | lastName : false 852 | }, 853 | card: { 854 | title : true, 855 | body : true 856 | }, 857 | user_card { 858 | user: "user", 859 | card: "card", 860 | owner: {type : "user", required: false} 861 | $uniques: { user_card: ["user", "card"] } 862 | } 863 | } 864 | ``` 865 | 866 | First, inserts users and cards. 867 | 868 | ```js 869 | var jsrel = JSRel.use('sample', {schema: schema}); 870 | 871 | var uTable = jsrel.table('user'); 872 | var shinout = uTable.ins({nickName: "shinout"}); 873 | var nishiko = uTable.ins({nickName: "nishiko"}); 874 | var cTable = jsrel.table('card'); 875 | var rabbit = uTable.ins({title: "rabbit", body: "It jumps!"}); 876 | var pot = uTable.ins({title: "pot", body: "a tiny yellow magic pot"}); 877 | ``` 878 | 879 | 880 | Then, inserts these relations. 881 | 882 | ```js 883 | var ucTable = jsrel.table('user_card'); 884 | ucTable.ins({ user: shinout, card: rabbit }); 885 | ``` 886 | 887 | 888 | We can also insert these relation like 889 | 890 | ```js 891 | ucTable.ins({ user_id: nishiko.id, card_id: pot.id }); 892 | ucTable.ins({ user_id: 1, card_id: 2 }); // 1: shinout, 2: pot 893 | ``` 894 | 895 | Remember that user_id and card_id are automatically generated and it represent the id column of each instance. 896 | When we pass an invalid id to these columns, an exception is thrown. 897 | 898 | ```js 899 | ucTable.ins({ user_id: 1, card_id: 5 }); // 1: shinout, 5: undefined! 900 | ``` 901 | 902 | When a relation column is not required, we can pass null. 903 | 904 | ```js 905 | ucTable.ins({ user: nishiko, card_id: 1, owner_id: null }); 906 | ``` 907 | 908 | When duplicated, **xxxx_id priors to xxxx** (where xxxx is the name of the original column). 909 | 910 | ```js 911 | ucTable.ins({ user: nishiko, user_id: 1, card_id: 1 }); // user_id => 1 912 | ``` 913 | 914 | #### inserting relations #### 915 | 916 | ```js 917 | obj.rel_table = [relObj1, relObj2, ...]; 918 | table.ins(obj); 919 | ``` 920 | relObj1, relObj2 are also inserted to table "rel_table" containing the new id as the external key. 921 | 922 | If the main table is related to the **rel_table** multiply, 923 | you must specify the column like 924 | 925 | ```js 926 | obj["rel_table.relcolumn"] = [relObj1, relObj2, ...]; 927 | table.ins(obj); 928 | ``` 929 | 930 | 931 | ### table.upd(obj, options) ### 932 | Updates an existing record. 933 | **obj** must contains **id** key. 934 | Only the valid keys (compatible with columns) in **obj** is updated. 935 | Throws **no** exceptions when you passes invalid keys. 936 | Throws an exception when you an invalid value with a valid key. 937 | 938 | Returns an instance of the updated record. 939 | It is NOT the same as the given argument. 940 | 941 | #### relation updates #### 942 | 943 | 944 | updating related tables 945 | ```js 946 | obj.rel = {id: 3 } 947 | obj.rel_id = 1 948 | // in this case, relObj is prior to rel_id 949 | table.upd(obj, {append: append}); 950 | 951 | var rel_id = table.one(obj, {id: obj.id}, {select: "rel_id"}); // 3. not 1 952 | ``` 953 | 954 | 955 | ```js 956 | obj.rel_table = [relObj1, relObj2, ...]; 957 | table.upd(obj, {append: append}); 958 | ``` 959 | 960 | if **relObj** contains "id" column, updating the object. 961 | Otherwise, inserting the object. 962 | If **options.append** is false or not given, already existing related objects are deleted. 963 | 964 | If the main table is related to the **rel_table** multiply, 965 | you must specify the column like 966 | 967 | ```js 968 | obj["rel_table.relcolumn"] = [relObj1, relObj2, ...]; 969 | table.upd(obj, {append: append}); 970 | ``` 971 | 972 | 973 | ### table.find(query, options) ### 974 | Selects records. 975 | Returns a list of records. 976 | **query** is an object to describe how to fetch records. 977 | 978 | 979 | #### query examples #### 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 |
exampledescription
{name: "shinout"}name must be equal to "shinout"
{name: ["shinout", "nishiko"]}name must be equal to "shinout" or "nishiko"
{name: {like$: "shin"}}name must be like "shin%"
{name: {$like: "inout"}}name must be like "%inout"
{name: [{$like: "inout"}, {equal: "nishiko"}] }name must be like "%inout" OR equals "nishiko"
{name: {$like: "inout", equal: "nishiko"} }name must be like "%inout" AND equals "nishiko"
{age: {gt: 24} }age must be greater than 24
{age: {gt: 24, le: 40} }age must be greater than 24 and less equal 40
{age: [{ge: 24}, {lt: 40}] }age must be greater equal 24 or less than 40
{country: {$in: ["Japan", "Korea"] }country must be one of "Japan", "Korea" (as "in" is a reserved word in JavaScript, used "$in" instead.)
{name: "shinout", age : {ge: 70 }must returns empty until shinout becomes 70
1018 | 1019 | 1020 | **options** is as follows. 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 |
keytypedescriptionexample
ordermixedsee order description{ name: "asc" }
limitintthe end position of the data20
offsetintoffset of the results10
joinmixedsee join description{records.scene: {title : {like$: "ABC"} }
selectstring (one of column names)get list of selected columns instead of objects"title"
selectarray (list of column names)get list of object which contains the given columns instead of all columns["name", "age"]
explainobjectput searching information to the given object{}
1065 | 1066 | #### order description #### 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 |
exampledescription
"age"order by age asc
{age: "desc"}order by age desc
{age: "desc", name: "asc"}order by age desc, name asc
1080 | 1081 | 1082 | #### results #### 1083 | Returns list of instances 1084 | 1085 | ```js 1086 | [ {id: 1, name: "shinout"}, {id: 2, name: "nishiko"}, ...] 1087 | ``` 1088 | 1089 | #### join description #### 1090 | sample data 1091 | 1092 |

group

1093 | 1094 | 1095 | 1096 | 1097 |
idname
1mindia
2ZZZ
1098 | 1099 |

user

1100 | 1101 | 1102 | 1103 | 1104 | 1105 |
idnameagegroup
1shinout251
2nishiko281
3xxx392
1106 | 1107 |

card

1108 | 1109 | 1110 | 1111 | 1112 | 1113 |
idtitlebody
1rabbitit jumps!
2pota tiny yellow magic pot
3PCcalculating...
1114 | 1115 |

user_card

1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 |
idusercard
111
221
312
423
533
1124 | 1125 | **Fetching N:1 related objects** 1126 | 1127 | ```js 1128 | var result = db.table('user').find({name: "shinout"}, {join: JOIN_VALUE}); 1129 | ``` 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1172 |
No.JOIN_VALUEdescriptionresult
1"group"get "group" column as object[{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}]
2{group : true}get "group" column as object (the same as sample1)[{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}]
3trueget all the related columns as object[{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}]
4{group : {name: {like$: "mind"}}}get "group" column as object whose name starts at "mind"[{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}]
5{group : {name: "ZZZ"}}get "group" column as object whose name is equal to "ZZZ"[] // empty
1173 | 1174 | 1175 | **Fetching 1:N related objects** 1176 | 1177 | var result = db.table('group').find({name: "mindia"}, {join: JOIN_VALUE}); 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | 1217 | 1218 | 1219 | 1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | 1249 | 1250 |
No.JOIN_VALUEdescriptionresult
6"user.group"get "user" table objects (setting the related column in "user" table)[{id: 1, name: "mindia", "user.group": [{id: 1, name: "shinout", age: 25}, {id: 2, name: "nishiko", age: 28}]}]
7"user"get "user" table objects (if related column is obvious)[{id: 1, name: "mindia", "user": [{id: 1, name: "shinout", age: 25}, {id: 2, name: "nishiko", age: 28}]}]
8{"user.group" : true }get "user" table objects (the same as sample6)[{id: 1, name: "mindia", "user.group": [{id: 1, name: "shinout", age: 25}, {id: 2, name: "nishiko", age: 28}]}]
9{"user.group" : {age : {gt: 27}} }get "user" table objects with age greater than 27[{id: 1, name: "mindia", "user.group": [{id: 2, name: "nishiko", age: 28}]}]
10{"user.group" : {age : {gt: 27}, as: "users"} }get "user" table objects with age greater than 27, with alias name "users"[{id: 1, name: "mindia", "users": [{id: 2, name: "nishiko", age: 28}]}]
11{"user.group" : {where : {age : {gt: 27}}, as: "users"} }get "user" table objects with age greater than 27, with alias name "users" (the canonical expression of sample9)[{id: 1, name: "mindia", "users": [{id: 2, name: "nishiko", age: 28}]}]
12{user : {age : {gt: 27}, as: "users"} }get "user" table objects with age greater than 27, with alias name "users"[{id: 1, name: "mindia", "users": [{id: 2, name: "nishiko", age: 28}]}]
13{user : {age : {gt: 47}, outer: true} }outer joining. Records containing Empty 1:N subqueries can be remained with the column filled with null.[{id: 1, name: "mindia", "user": null}]
13{user : {age : {gt: 47}, outer: "array"} }outer joining. Records containing Empty 1:N subqueries can be remained with the column filled with empty array.[{id: 1, name: "mindia", "user": [] }]
1251 | 1252 | **Fetching N:M related objects** 1253 | 1254 | var result = db.table('user').find({name: "shinout"}, {join: JOIN_VALUE}); 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 |
15{"card": {via: "user_card"} }get "card" related through "user_card"[{id: 1, name: "shinout", "card": [ {id:1, ...}, {id: 3, ...}] }]
1264 | 1265 | ### table.one(id) ### 1266 | Gets one object by id. 1267 | 1268 | ### table.one(query, options) ### 1269 | Gets one result by **table.find()**. 1270 | 1271 | 1272 | ### table.del(id) ### 1273 | Deletes a record with a given **id** . 1274 | 1275 | 1276 | ### table.del(query) ### 1277 | Deletes records with a given **query** . 1278 | **query** is the same argument as **table.find(query)**. 1279 | 1280 | 1281 | #### relation handling in deletion #### 1282 | When a record is deleted, related records are also deleted. 1283 | 1284 | 1285 | Think upon the schema. 1286 | 1287 | First, inserts users, cards and these relations. 1288 | 1289 | ```js 1290 | var jsrel = JSRel.use('sample', {schema: schema}); 1291 | 1292 | var uTable = jsrel.table('user'); 1293 | var cTable = jsrel.table('card'); 1294 | var ucTable = jsrel.table('user_card'); 1295 | 1296 | var shinout = uTable.ins({nickName: "shinout"}); 1297 | var nishiko = uTable.ins({nickName: "nishiko"}); 1298 | 1299 | var rabbit = uTable.ins({title: "rabbit", body: "It jumps!"}); 1300 | var pot = uTable.ins({title: "pot", body: "a tiny yellow magic pot"}); 1301 | 1302 | ucTable.ins({ user: shinout, card: rabbit }); 1303 | ucTable.ins({ user: nishiko, card: rabbit }); 1304 | ucTable.ins({ user: shinout, card: pot }); 1305 | ``` 1306 | 1307 | 1308 | Next, delete shinout. 1309 | 1310 | ```js 1311 | uTable.del(shinout); 1312 | ``` 1313 | 1314 | Then, the dependent records ( shinout-rabbit, shinout-pot ) are also removed. 1315 | 1316 | ```js 1317 | ucTable.find().length; // 1 (nishiko-rabbit) 1318 | ``` 1319 | 1320 | 1321 | shortcut 1322 | -------- 1323 | 1324 | - jsrel.ins(tableName, ...) 1325 | - jsrel.upd(tableName, ...) 1326 | - jsrel.find(tableName, ...) 1327 | - jsrel.one(tableName, ...) 1328 | - jsrel.del(tableName, ...) 1329 | 1330 | are, select table via jsrel.table(tableName) in the first place. 1331 | Then run the operation using the remaining arguments. 1332 | 1333 | for example, 1334 | 1335 | ```js 1336 | jsre.ins('user', {nickName: "shinout"}); 1337 | ``` 1338 | 1339 | is completely equivalent to 1340 | 1341 | ```js 1342 | jsrel.table('user').ins({nickName: "shinout"}); 1343 | ``` 1344 | 1345 | 1346 | make it standalone 1347 | -------------------- 1348 | **[standalone](https://github.com/shinout/standalone)** is a library to make a worker process / thread which can communicate with master. 1349 | 1350 | Here are the basic concept. 1351 | 1352 | master.js 1353 | 1354 | ```js 1355 | standalone("worker.js", function(model) { 1356 | 1357 | model.getSongsByArtist("the Beatles", function(err, songs) { 1358 | console.log(songs); 1359 | }); 1360 | 1361 | }); 1362 | ``` 1363 | 1364 | 1365 | worker.js 1366 | 1367 | ```js 1368 | var db = JSRel.use("xxx", {schema: { 1369 | artist: {name: true}, 1370 | song : {title: true, artist: "artist"} 1371 | }}); 1372 | var btls = db.ins("artist", {name: "the Beatles"}); 1373 | db.ins("song", {title: "Help!", artist: btls}); 1374 | db.ins("song", {title: "In My Life", artist: btls}); 1375 | 1376 | var model = { 1377 | getSongsByArtist: function(name) { 1378 | return db.find("artist", {name : name}, {join: "song", select : "song"}); 1379 | } 1380 | }; 1381 | standalone(model); 1382 | ``` 1383 | 1384 | In master.js, we can use "getSongsByArtist" asynchronously, catching possible errors in err. 1385 | 1386 | In Node.js, **standalone** spawns a child process. 1387 | 1388 | In browsers, **standalone** creates a WebWorker instance. 1389 | 1390 | In Titanium, standalone is not supported. 1391 | 1392 | ### environmental specific code ### 1393 | 1394 | Because Node.js and WebWorker has a different requiring system, 1395 | We must be careful of loading scripts. 1396 | 1397 | 1398 | in Node.js (worker.js) 1399 | 1400 | ```js 1401 | var JSRel = require('jsrel'); 1402 | var standalone = require('standalone'); 1403 | ``` 1404 | 1405 | This is enough. 1406 | 1407 | in browsers (worker.js) 1408 | 1409 | ```js 1410 | importScripts('/pathto/SortedList.js', '/pathto/jsrel.js', '/pathto/standalone.js'); 1411 | ``` 1412 | 1413 | Don't forget to import **[SortedList](https://github.com/shinout/SortedList)** (which JSRel depends on). 1414 | 1415 | 1416 | LICENSE 1417 | ------- 1418 | (The MIT License) 1419 | 1420 | Copyright (c) 2012 SHIN Suzuki 1421 | 1422 | Permission is hereby granted, free of charge, to any person obtaining 1423 | a copy of this software and associated documentation files (the 1424 | 'Software'), to deal in the Software without restriction, including 1425 | without limitation the rights to use, copy, modify, merge, publish, 1426 | distribute, sublicense, and/or sell copies of the Software, and to 1427 | permit persons to whom the Software is furnished to do so, subject to 1428 | the following conditions: 1429 | 1430 | The above copyright notice and this permission notice shall be 1431 | included in all copies or substantial portions of the Software. 1432 | 1433 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 1434 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 1435 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 1436 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 1437 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 1438 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 1439 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1440 | -------------------------------------------------------------------------------- /benchmarks/assert.js: -------------------------------------------------------------------------------- 1 | function assert2(torf) { return torf } 2 | 3 | function err() { 4 | var args = Array.prototype.slice.call(arguments); 5 | args.unshift('[JSRel.js]'); 6 | var err = args.join(" "); 7 | if (!err) err = "(undocumented error)"; 8 | throw new Error(err); 9 | } 10 | 11 | function quo(v) { return '"'+ v + '"'} 12 | 13 | function assert1() { 14 | var args = Array.prototype.slice.call(arguments); 15 | var torf = args.shift(); 16 | if (torf) return; 17 | args.unshift('[JSRel.js]'); 18 | var err = args.join(" "); 19 | if (!err) err = "(undocumented error)"; 20 | throw new Error(err); 21 | } 22 | 23 | function test(N) { 24 | var i; 25 | console.time("assert1"); 26 | for (i=0; i= 5000 as it's too slow to get results 55 | start = new Date 56 | db.one(table, {name: "upd" + id}) for id in [1..limit] 57 | result = resultDB.ins("result", operation: "searching", table: table, version: version, length: limit, time: new Date - start) 58 | console.log "\t\t\t", "searching", result.time, "sec" 59 | 60 | # delete 61 | if limit < 12000 # omit deleting when records >= 12000 as it's too slow to execute 62 | start = new Date 63 | db.del(table, id) for id in [1..limit] 64 | result = resultDB.ins("result", operation: "deletion", table: table, version: version, length: limit, time: new Date - start) 65 | console.log "\t\t\t", "deletion", result.time, "sec" 66 | 67 | 68 | ### 69 | # show results 70 | ### 71 | # old vs current 72 | console.log "===================================================" 73 | console.log "========= COMPARING OLD AND CURRENT =============" 74 | console.log "===================================================" 75 | 76 | console.log ["operation", "table", "length", oldVersion, currentVersion, "#{oldVersion}/#{currentVersion}"].join("\t") 77 | for operation in ["insertion", "updating", "searching", "deletion"] 78 | for table in ["index", "noindex"] 79 | for length in limits 80 | rs = resultDB.find "result", {operation: operation, table: table, length: length}, {order: "version"} 81 | continue if rs.length isnt 2 82 | cur = if rs[0].version is oldVersion then rs[1] else rs[0] 83 | old = if rs[0].version is oldVersion then rs[0] else rs[1] 84 | 85 | rate = Math.round(old.time*1000/cur.time)/1000 86 | color = if rate>=1 then "green" else "red" 87 | console[color] [operation, table, length, old.time, cur.time, rate].join("\t") 88 | 89 | # index vs noindex 90 | console.log "===================================================" 91 | console.log "========= COMPARING INDEX AND NOINDEX ==========" 92 | console.log "===================================================" 93 | 94 | console.log ["operation", "version", "length", "noindex", "index", "index/noindex"].join("\t") 95 | for operation in ["insertion", "updating", "searching", "deletion"] 96 | for version of dbs 97 | for length in limits 98 | rs = resultDB.find "result", {operation: operation, version: version, length: length}, {order: "table"} 99 | continue if rs.length isnt 2 100 | idx = rs[0] 101 | nidx = rs[1] 102 | rate = Math.round(nidx.time*1000/idx.time)/1000 103 | color = if rate>=1 then "green" else "red" 104 | console[color] [operation, version, length, nidx.time, idx.time, rate].join("\t") 105 | -------------------------------------------------------------------------------- /benchmarks/escape.js: -------------------------------------------------------------------------------- 1 | function replace(v) { 2 | return '"' + v + '"'; 3 | } 4 | 5 | var str = 'fasdeib"fa"fasd "table" is not defined "" is empty "" is not valid""""'; 6 | 7 | console.log(str.replace(/"/g, '\\"')); 8 | console.log(str.split('"').join('\\"')); 9 | console.assert(str.split('"').join('\\"') === str.replace(/"/g, '\\"')); 10 | eval('var evalStr = "' + str.split('"').join('\\"') + '"'); 11 | console.log(evalStr); 12 | console.assert(evalStr === str); 13 | 14 | function test(N) { 15 | var i, v; 16 | console.time("replace"); 17 | for (i=0; i= M, "N < M!!!!!"); 4 | // creating array 5 | var arr = new Array(N); 6 | for (var i=0; i b ? 1: -1; 40 | }); 41 | } 42 | console.timeEnd("sort"); 43 | } 44 | 45 | for (var N="10"; N!="100000"; N+= "0") { 46 | var n = Number(N); 47 | var nper2 = Math.floor(n/2); 48 | order(n, nper2, 1000); 49 | var nper3 = Math.floor(n/3); 50 | order(n, nper3, 1000); 51 | var nper30 = Math.floor(n*3 / 10); 52 | order(n, nper30, 1000); 53 | var nper4 = Math.floor(n/4); 54 | order(n, nper4, 1000); 55 | 56 | var nper5 = Math.floor(n/5); 57 | order(n, nper5, 1000); 58 | 59 | var nper6 = Math.floor(n/6); 60 | order(n, nper6, 1000); 61 | 62 | var nper7 = Math.floor(n/7); 63 | order(n, nper7, 1000); 64 | } 65 | 66 | function cap(arr) { 67 | if (!arr.length) return []; 68 | var current = ret = arr.shift(), target; 69 | var n = 0, len = arr.length; 70 | while(n < len) { 71 | ret = [], target = arr[n++]; 72 | for(var i=0, l=current.length; i", 3 | "name": "jsrel", 4 | "description": "JavaScript lightweight synchronous RDB", 5 | "keywords": ["database", "DB", "RDB", "relation"], 6 | "version": "0.4.1", 7 | "repository": { 8 | "url": "git://github.com/shinout/jsrel" 9 | }, 10 | "main": "lib/jsrel.js", 11 | "scripts": { 12 | "test": "vows test/*.js" 13 | }, 14 | "bin": { 15 | "install-jsrel": "./bin/install-jsrel.sh" 16 | }, 17 | 18 | "engines": { 19 | "node": ">=0.6.1" 20 | }, 21 | "dependencies": { 22 | "sortedlist": ">=0.3.1" 23 | }, 24 | "devDependencies": { 25 | "vows" : ">=0.6.2", 26 | "linestream" : ">=0.3.2", 27 | "grunt": "^0.4.4", 28 | "grunt-contrib-coffee": "^0.10.1" 29 | }, 30 | "optionalDependencies": {} 31 | } 32 | -------------------------------------------------------------------------------- /src/jsrel.coffee: -------------------------------------------------------------------------------- 1 | ((root, factory) -> 2 | ### 3 | # for AMD (Asynchronous Module Definition) 4 | ### 5 | if typeof define is "function" and define.amd 6 | define ["sortedlist"], factory 7 | else if typeof module is "object" and module.exports 8 | module.exports = factory() 9 | else 10 | root.JSRel = factory(root.SortedList) 11 | return 12 | ) this, (SortedList) -> 13 | 14 | ###################### 15 | # ENVIRONMENTS 16 | ###################### 17 | SortedList = require("sortedlist") unless SortedList 18 | isTitanium = (typeof Ti is "object" and typeof Titanium is "object" and Ti is Titanium) 19 | isNode = not isTitanium and (typeof module is "object" and typeof exports is "object" and module.exports is exports) 20 | isBrowser = (typeof localStorage is "object" and typeof sessionStorage is "object") 21 | storages = mock: do-> 22 | mockData = {} 23 | getItem: (id) -> 24 | mockData[id] or null 25 | 26 | setItem: (id, data) -> 27 | mockData[id] = data 28 | return 29 | 30 | removeItem: (id) -> 31 | delete mockData[id] 32 | 33 | if isBrowser 34 | storages.local = window.localStorage 35 | storages.session = window.sessionStorage 36 | if isTitanium 37 | fs = Ti.Filesystem 38 | storages.file = 39 | getItem: (k) -> 40 | file = fs.getFile(k.toString()) 41 | if file.exists() then fs.getFile(k.toString()).read().text else null 42 | 43 | setItem: (k, v) -> 44 | fs.getFile(k.toString()).write v.toString() 45 | 46 | removeItem: (k) -> 47 | fs.getFile(k.toString()).deleteFile() 48 | else if isNode 49 | fs = require("fs") 50 | storages.file = 51 | getItem: (k) -> 52 | try 53 | return fs.readFileSync(k, "utf8") 54 | catch e 55 | return null 56 | return 57 | 58 | setItem: (k, v) -> 59 | fs.writeFileSync k, v.toString(), "utf8" 60 | 61 | removeItem: (k) -> 62 | fs.unlinkSync k 63 | 64 | ###################### 65 | # UTILITIES FOR DEFINING CLASSES 66 | ###################### 67 | 68 | # defineGetters : define getters to object 69 | defineGetters = (obj, getters)-> 70 | Object.defineProperty(obj, name, get: fn, set: noop) for name, fn of getters 71 | 72 | # defineConstants : define constants to object 73 | defineConstants = (obj, constants)-> 74 | for name, val of constants 75 | Object.defineProperty(obj, name, value: val, writable: false) 76 | Object.freeze val if typeof val is "object" 77 | 78 | ###################### 79 | # class JSRel 80 | ###################### 81 | ### 82 | # public 83 | # - id 84 | # - name 85 | # - tables : list of tables (everytime dynamically created) 86 | # 87 | # private 88 | # - _storage : storage name 89 | # - _autosave : boolean 90 | # - _tblInfos : { tableName => Table object } 91 | # - _hooks : { eventName => [function, function...] } 92 | ### 93 | class JSRel 94 | # key: id of db, value: instance of JSRel 95 | @._dbInfos = {} 96 | 97 | ### 98 | # class properties 99 | # uniqIds: list of uniqIds 100 | # isNode, isTitanium, isBrowser: environment detection. boolean 101 | # storage: available storages (array) 102 | ### 103 | defineGetters @, 104 | uniqIds: -> Object.keys @_dbInfos 105 | 106 | defineConstants @, 107 | isNode : isNode 108 | isTitanium : isTitanium 109 | isBrowser : isBrowser 110 | storages : storages 111 | 112 | ### 113 | # constructor 114 | # 115 | # called only from JSRel.use or JSRel.$import 116 | # arguments 117 | # - uniqId : 118 | # - name : 119 | # - storage : 120 | # - autosave : 121 | # - format : format of tblData to parse (one of Raw, Schema, Compressed) 122 | # - tblData : 123 | # - loaded : if loaded from stored data, true 124 | ### 125 | constructor: (uniqId, name, @_storage, @_autosave, format, tblData, loaded) -> 126 | defineConstants @, id: uniqId, name: name 127 | 128 | @constructor._dbInfos[uniqId] = db: this, storage: @_storage 129 | 130 | @_hooks = {} 131 | @_tblInfos = {} 132 | @_loaded = !!loaded 133 | 134 | for tblName, colData of tblData 135 | @_tblInfos[tblName] = new Table(tblName, this, colData, format) 136 | 137 | ### 138 | # JSRel.use(uniqId, option) 139 | # 140 | # Creates instance if not exist. Gets previously created instance if already exists 141 | # - uniqId: the identifier of the instance, used for storing the data to external system(file, localStorage...) 142 | # - options: 143 | # - storage(string) : type of external storage. one of mock, file, local, session 144 | # - schema (object) : DB schema. See README.md for detailed information 145 | # - reset (boolean) : if true, create db even if previous db with the same uniqId already exists. 146 | # - autosave (boolean) : if true, save at every action(unstable...) 147 | # - name (string) : name of the db 148 | # 149 | # - __create (boolean) : throws an error if db already exists. 150 | ### 151 | @use : (uniqId, options = {}) -> 152 | uniqId or err "uniqId is required and must be non-zero value." 153 | uniqId = uniqId.toString() 154 | 155 | # if given uniqId already exists in memory, load it 156 | storedInMemory = @_dbInfos[uniqId] 157 | if storedInMemory? 158 | err "uniqId", quo(uniqId), "already exists" if options.__create 159 | return @_dbInfos[uniqId].db if not options or not options.reset 160 | 161 | #options or err "options is required." 162 | options.storage = options.storage or if (isNode or isTitanium) then "file" else if isBrowser then "local" else "mock" 163 | storage = @storages[options.storage] 164 | storage or err "options.storage must be one of " + Object.keys(@storages).map(quo).join(",") 165 | 166 | if not options.reset and dbJSONstr = storage.getItem(uniqId) 167 | JSRel.$import uniqId, dbJSONstr, force : false 168 | else 169 | options.schema and typeof options.schema is "object" or err "options.schema is required" 170 | Object.keys(options.schema).length or err "schema must contain at least one table" 171 | format = "Schema" 172 | tblData = deepCopy(options.schema) 173 | name = if options.name? then options.name.toString() else uniqId 174 | new JSRel(uniqId, name, options.storage, !!options.autosave, format, tblData) 175 | 176 | @createIfNotExists = @use 177 | 178 | ### 179 | # JSRel.create(uniqId, option) 180 | # 181 | # Creates instance if not exist. Throws an error if already exists 182 | # - uniqId: the identifier of the instance, used for storing the data to external system(file, localStorage...) 183 | # - options: 184 | # - storage(string) : type of external storage. one of mock, file, local, session 185 | # - schema (object) : DB schema. See README.md for detailed information 186 | # - autosave (boolean) : if true, save at every action(unstable...) 187 | # - name (string) : name of the db 188 | ### 189 | @create : (uniqId, options) -> 190 | options or (options = {}) 191 | delete options.reset 192 | options.__create = true 193 | JSRel.use uniqId, options 194 | 195 | ### 196 | # JSRel.$import(uniqId, dbJSONstr, options) 197 | # 198 | # Creates instance from saved data 199 | # - uniqId: the identifier of the instance, used for storing the data to external system(file, localStorage...) 200 | # - dbJSONstr : data 201 | # - options: 202 | # - force (boolean) : if true, overrides already-existing database. 203 | # - storage(string) : type of external storage. one of mock, file, local, session 204 | # - autosave (boolean) : if true, save at every action(unstable...) 205 | # - name (string) : name of the db 206 | ### 207 | @$import : (uniqId, dbJSONstr, options = {}) -> 208 | uniqId or err "uniqId is required and must be non-zero value." 209 | uniqId = uniqId.toString() 210 | (options.force or not @_dbInfos[uniqId]?) or err "id", quo(uniqId), "already exists" 211 | try 212 | d = JSON.parse(dbJSONstr) 213 | catch e 214 | err "Invalid format given to JSRel.$import" 215 | for key in [ "n","s","a","f","t" ] 216 | d.hasOwnProperty(key) or err("Invalid Format given.") 217 | 218 | # trying to use given autosave, name and storage 219 | autosave = if options.autosave? then !!options.autosave else d.a 220 | name = if options.name? then options.name.toString() else d.n 221 | storage = if options.storage? then options.storage.toString() else d.s 222 | JSRel.storages[storage]? or err "options.storage must be one of " + Object.keys(JSRel.storages).map(quo).join(",") 223 | 224 | new JSRel(uniqId, name, storage, autosave, d.f, d.t, true) # the last "true" means "loaded" 225 | 226 | # alias 227 | @import = JSRel.$import 228 | 229 | ####### 230 | ## 231 | ## JSRel instance properties (getter) 232 | ## 233 | ####### 234 | defineGetters @::, 235 | loaded : -> @_loaded 236 | created: -> !@_loaded 237 | 238 | storage: -> JSRel.storages[@_storage] 239 | 240 | tables : -> Object.keys @_tblInfos 241 | 242 | schema : -> 243 | tableDescriptions = {} 244 | for tableName, tblInfo of @_tblInfos 245 | table = @_tblInfos[tableName] 246 | columnDescriptions = {} 247 | for colName in table.columns 248 | continue if Table.AUTO_ADDED_COLUMNS[colName] #id, ins_at, upd_at 249 | colInfo = table._colInfos[colName] 250 | columnDescriptions[colName] = 251 | type : Table.TYPES[colInfo.type] 252 | required: colInfo.required 253 | _default: colInfo._default 254 | 255 | columnDescriptions.$indexes = [] 256 | columnDescriptions.$uniques = [] 257 | for col, index of table._indexes 258 | continue if Table.AUTO_ADDED_COLUMNS[colName] 259 | columnDescriptions[(if index._unique then "$uniques" else "$indexes")].push col.split(",") 260 | 261 | columnDescriptions.$classes = Object.keys(table._classes).map((col) -> col.split ",") 262 | 263 | for metaKey in Table.COLUMN_META_KEYS 264 | delete columnDescriptions[metaKey] if columnDescriptions[metaKey].length is 0 265 | 266 | tableDescriptions[tableName] = columnDescriptions 267 | tableDescriptions 268 | 269 | ####### 270 | ## 271 | ## JSRel instance methods 272 | ## 273 | ####### 274 | 275 | ### 276 | # JSRel#table(tableName) 277 | # gets table ofject by its name 278 | ### 279 | table : (tableName) -> 280 | @_tblInfos[tableName] 281 | 282 | ### 283 | # JSRel#save(noCompress) 284 | ### 285 | save : (noCompress) -> 286 | @_hooks["save:start"] and @_emit "save:start", @origin() 287 | data = @$export(noCompress) 288 | @storage.setItem @id, data 289 | @_emit "save:end", data 290 | this 291 | 292 | ### 293 | # JSRel#origin() 294 | ### 295 | origin : -> @storage.getItem @id 296 | 297 | ### 298 | # JSRel#$export(noCompress) 299 | ### 300 | $export : (noCompress) -> 301 | ret = 302 | n: @name 303 | s: @_storage 304 | a: @_autosave 305 | 306 | ret.t = if noCompress then @_tblInfos else do(tblData = @_tblInfos)-> 307 | t = {} 308 | for tblName, table of tblData 309 | t[tblName] = table._compress() 310 | return t 311 | 312 | ret.f = if (noCompress) then "Raw" else "Compressed" 313 | JSON.stringify ret 314 | 315 | # alias for $export 316 | export : (noCompress)-> @$export noCompress 317 | 318 | ### 319 | # JSRel#toSQL(options) 320 | ### 321 | toSQL : (options = type: "mysql", engine: "InnoDB") -> 322 | if options.rails 323 | n2s = (n) -> ("000" + n).match /..$/ 324 | datetime = (v) -> 325 | t = new Date(v) 326 | t.getFullYear() + "-" + 327 | n2s(t.getMonth() + 1) + "-" + 328 | n2s(t.getDate()) + " " + 329 | n2s(t.getHours()) + ":" + 330 | n2s(t.getMinutes()) + ":" + 331 | n2s(t.getSeconds()) 332 | 333 | (options.columns) or (options.columns = {}) 334 | (options.values) or (options.values = {}) 335 | options.columns.upd_at = "updated_at" 336 | options.columns.ins_at = "created_at" 337 | options.values.upd_at = datetime 338 | options.values.ins_at = datetime 339 | ret = [] 340 | if options.db 341 | dbname = (if options.db is true then @id else options.db.toString()) 342 | ret.push "CREATE DATABASE `" + dbname + "`;" 343 | ret.push "USE `" + dbname + "`;" 344 | tables = @tables 345 | if not options.noschema and not options.nodrop 346 | ret.push tables.map((tbl) -> 347 | @table(tbl)._toDropSQL options 348 | , this).reverse().join("\n") 349 | unless options.noschema 350 | ret.push tables.map((tbl) -> 351 | @table(tbl)._toCreateSQL options 352 | , this).join("\n") 353 | unless options.nodata 354 | ret.push tables.map((tbl) -> 355 | @table(tbl)._toInsertSQL options 356 | , this).join("\n") 357 | ret.join "\n" 358 | 359 | ### 360 | # JSRel#on() 361 | ### 362 | on : (evtname, fn, options = {}) -> 363 | @_hooks[evtname] = [] unless @_hooks[evtname] 364 | @_hooks[evtname][(if options.unshift then "unshift" else "push")] fn 365 | return 366 | 367 | ### 368 | # JSRel#off() 369 | ### 370 | off : (evtname, fn) -> 371 | return unless @_hooks[evtname] 372 | return @_hooks[evtname] = null unless fn? 373 | @_hooks[evtname] = @_hooks[evtname].filter((f) -> fn isnt f) 374 | return 375 | 376 | ### 377 | # JSRel#drop() 378 | ### 379 | drop : (tblNames...)-> 380 | nonRequiredReferringTables = {} 381 | for tblName in tblNames 382 | table = @_tblInfos[tblName] 383 | table or err "unknown table name", quo(tblName), "in jsrel#drop" 384 | for refTblName, refTables of table._referreds 385 | for col, colInfo of refTables 386 | unless colInfo 387 | nonRequiredReferringTables[refTblName] = col 388 | else if refTblName not in tblNames 389 | err("table ", quo(tblName), "has its required-referring table", quo(refTblName), ", try jsrel#drop('" + tblName + "', '" + refTblName + "')") 390 | 391 | for tblName in tblNames 392 | table = @_tblInfos[tblName] 393 | for relname, relTblName of table._rels 394 | continue if relTblName in tblNames # skip if related table is already in deletion list 395 | relTable = @_tblInfos[relTblName] 396 | delete relTable._referreds[tblName] 397 | for prop in ["_colInfos", "_indexes", "_idxKeys", "_classes", "_data", "_rels", "_referreds"] 398 | delete table[prop] 399 | delete @_tblInfos[tblName] 400 | 401 | for refTblName, col of nonRequiredReferringTables 402 | refTable = @_tblInfos[refTblName] 403 | for id, record of refTable._data 404 | record[col + "_id"] = null 405 | return 406 | 407 | #### 408 | # private instance methods 409 | #### 410 | 411 | ### 412 | # JSRel#_emit() 413 | ### 414 | _emit : (args...)-> 415 | evtname = args.shift() 416 | return unless Array.isArray @_hooks[evtname] 417 | for fn in @_hooks[evtname] 418 | fn.apply this, args 419 | return 420 | 421 | ###################### 422 | # class Table 423 | ###################### 424 | ### 425 | # public 426 | # - columns : list of columns 427 | # - name : table name 428 | # - db : id of the parent JSRel (externally set) 429 | # 430 | # private 431 | # - _colInfos : { colName => column Info object } 432 | # - _indexes : { columns => sorted list } 433 | # - _idxKeys : { column => list of idx column sets} 434 | # - _classes : { columns => classes hash object} 435 | # - _data : { id => record } 436 | # - _rels : { column => related table name } 437 | # - _referreds : { referring table name => { column => required or not} } (externally set) 438 | ### 439 | class Table 440 | 441 | ### 442 | # class properties 443 | ### 444 | defineConstants @, 445 | _BOOL : 1 446 | _NUM : 2 447 | _STR : 3 448 | _INT : 4 449 | _CHRS : 5 450 | _CHR2 : 6 451 | TYPES: 452 | 1: "boolean" 453 | 2: "number" 454 | 3: "string" 455 | 4: "number" 456 | 5: "string" 457 | 6: "string" 458 | TYPE_SQLS: 459 | 1: "tinyint(1)" 460 | 2: "double" 461 | 3: "text" 462 | 4: "int" 463 | 5: "varchar(255)" 464 | 6: "varchar(160)" 465 | INVALID_COLUMNS: 466 | [ "id", "ins_at", "upd_at" 467 | "on", "off" 468 | "str", "num", "bool", "int", "float", "text", "chars", "double", "string", "number", "boolean" 469 | "order", "limit", "offset", "join", "where", "as", "select", "explain" 470 | ] 471 | COLKEYS: [ "name", "type", "required", "_default", "rel", "sqltype" ] 472 | COLUMN_META_KEYS: ["$indexes", "$uniques", "$classes"] 473 | AUTO_ADDED_COLUMNS: id: true, ins_at: true, upd_at: true 474 | NOINDEX_MIN_LIMIT: 100 475 | ID_TEMP: 0 476 | CLASS_EXISTING_VALUE: 1 477 | 478 | ### 479 | # constructor 480 | # 481 | # arguments 482 | # name : (string) table name 483 | # db : (JSRel) 484 | # colData : table information 485 | # format : format of tblData to parse (one of Raw, Schema, Compressed) 486 | # 487 | ### 488 | constructor: (name, db, colData, format) -> 489 | defineConstants @, name: name, db: db 490 | 491 | @_colInfos = {} 492 | @_data = {} 493 | @_indexes = {} 494 | @_idxKeys = {} 495 | @_classes = {} 496 | @_rels = {} 497 | @_referreds = {} 498 | 499 | (typeof @["_parse" + format] is "function") or err("unknown format", quo(format), "given in", quo(@db.id)) 500 | @["_parse" + format] colData 501 | 502 | columns = Object.keys(@_colInfos).sort() 503 | colOrder = {} 504 | colOrder[col] = k for col, k in columns 505 | defineConstants(@, columns: columns, colOrder: colOrder) 506 | 507 | ####### 508 | ## 509 | ## Table instance methods 510 | ## 511 | ####### 512 | 513 | ### 514 | # Table#ins() 515 | ### 516 | ins : (argObj, options = {}) -> 517 | err "You must pass object to table.ins()." unless (argObj and typeof argObj is "object") 518 | @_convertRelObj argObj 519 | 520 | # id, ins_at, upd_at are removed unless options.force 521 | unless options.force 522 | delete argObj[col] for col of Table.AUTO_ADDED_COLUMNS 523 | else 524 | argObj[col] = Number(argObj[col]) for col of Table.AUTO_ADDED_COLUMNS when col of argObj 525 | 526 | insObj = {} 527 | for col in @columns 528 | insObj[col] = argObj[col] 529 | @_cast col, insObj 530 | 531 | # checking relation tables' id 532 | for col, relTblName of @_rels 533 | idcol = col + "_id" 534 | exId = insObj[idcol] 535 | relTable = @db.table(relTblName) 536 | required = @_colInfos[idcol].required 537 | continue if not required and not exId? 538 | exObj = relTable.one(exId) 539 | 540 | if not required and not exObj? 541 | insObj[idcol] = null 542 | else if exObj is null 543 | err "invalid external id", quo(idcol), ":", exId 544 | 545 | # setting id, ins_at, upd_at 546 | if insObj.id? 547 | err("the given id \"", insObj.id, "\" already exists.") if @_data[insObj.id]? 548 | err("id cannot be", Table.ID_TEMP) if insObj.id is Table.ID_TEMP 549 | else 550 | insObj.id = @_getNewId() 551 | insObj.ins_at = new Date().getTime() unless insObj.ins_at? 552 | insObj.upd_at = insObj.ins_at unless insObj.upd_at? 553 | 554 | @_data[insObj.id] = insObj 555 | 556 | # inserting indexes, classes 557 | try 558 | @_checkUnique idxName, insObj for idxName of @_indexes 559 | catch e 560 | delete @_data[insObj.id] 561 | throw e 562 | return null 563 | 564 | sortedList.insert insObj.id for idxName, sortedList of @_indexes 565 | 566 | for columns, cls of @_classes 567 | values = columns.split(",").map((col) -> insObj[col]).join(",") 568 | cls[values] = {} unless cls[values] 569 | cls[values][insObj.id] = Table.CLASS_EXISTING_VALUE 570 | 571 | # firing event (FOR PERFORMANCE, existing check @db._hooks runs before emitting) 572 | @db._hooks["ins"] and @db._emit "ins", @name, insObj 573 | @db._hooks["ins:" + @name] and @db._emit "ins:" + @name, insObj 574 | 575 | # inserting relations 576 | for exTblName, referred of @_referreds 577 | cols = Object.keys referred 578 | insertObjs = {} 579 | if cols.length is 1 580 | relatedObjs = argObj[exTblName] or argObj[exTblName + "." + cols[0]] 581 | (insertObjs[cols[0]] = if Array.isArray relatedObjs then relatedObjs else [relatedObjs]) if relatedObjs 582 | else 583 | for col in cols 584 | relatedObjs = argObj[exTblName + "." + col] 585 | (insertObjs[col] = if Array.isArray relatedObjs then relatedObjs else [relatedObjs]) if relatedObjs 586 | 587 | for col, relatedObjs of insertObjs 588 | exTable = @db.table(exTblName) 589 | for relObj in relatedObjs 590 | relObj[col + "_id"] = insObj.id 591 | exTable.ins relObj 592 | 593 | # autosave, returns copy 594 | @db.save() if @db._autosave 595 | copy insObj 596 | 597 | ### 598 | # Table#upd() 599 | ### 600 | upd : (argObj, options = {}) -> 601 | err "id is not found in the given object." if argObj is null or argObj.id is null or argObj.id is Table.ID_TEMP #TODO update without id 602 | argObj.id = Number(argObj.id) # TODO do not modify argument object 603 | oldObj = @_data[argObj.id] 604 | err "Cannot update. Object not found in table", @name, "with given id", argObj.id if oldObj is null 605 | 606 | # delete timestamp (prevent manual update) 607 | unless options.force 608 | delete argObj.ins_at 609 | delete argObj.upd_at 610 | else 611 | argObj.ins_at = Number(argObj.ins_at) if "ins_at" of argObj 612 | argObj.upd_at = new Date().getTime() 613 | 614 | # create new update object and decide which columns to update 615 | @_convertRelObj argObj 616 | updObj = id: argObj.id 617 | updCols = [] 618 | for col in @columns 619 | if argObj.hasOwnProperty(col) 620 | updVal = argObj[col] 621 | updObj[col] = updVal 622 | updCols.push col if updVal isnt oldObj[col] 623 | @_cast col, argObj 624 | else 625 | updObj[col] = oldObj[col] 626 | 627 | # udpate table with relation 628 | for updCol in updCols 629 | relTblName = @_rels[updCol] 630 | continue unless relTblName 631 | idcol = updCol + "_id" 632 | if idcol of updObj 633 | exId = updObj[idcol] 634 | required = @_colInfos[idcol].required 635 | continue if not required and not exId? 636 | exObj = @db.one(relTblName, exId) 637 | if not required and not exObj? 638 | updObj[idcol] = null 639 | else if exObj is null 640 | err "invalid external id", quo(idcol), ":", exId 641 | 642 | ## udpate indexes, classes 643 | # removing old index 644 | # TODO don't remove index when the key is id 645 | updIndexPoses = {} 646 | for updCol in updCols 647 | idxNames = @_idxKeys[updCol] 648 | continue unless idxNames 649 | for idxName in idxNames 650 | list = @_indexes[idxName] 651 | # getting old position and remove it 652 | for position in list.keys(updObj.id) 653 | if list[position] is updObj.id 654 | updIndexPoses[idxName] = position 655 | list.remove position 656 | break 657 | 658 | @_data[argObj.id] = updObj 659 | 660 | # checking unique 661 | try 662 | for updCol in updCols 663 | idxNames = @_idxKeys[updCol] 664 | continue unless idxNames 665 | @_checkUnique idxName, updObj for idxName in idxNames 666 | # rollbacking 667 | catch e 668 | @_data[argObj.id] = oldObj 669 | @_indexes[idxName].insert oldObj.id for idxName of updIndexPoses 670 | throw e 671 | # update indexes 672 | @_indexes[idxName].insert argObj.id for idxName of updIndexPoses 673 | 674 | # update classes 675 | for columns, cls of @_classes 676 | cols = columns.split(",") 677 | toUpdate = false 678 | toUpdate = true for clsCol in cols when clsCol in updCols 679 | continue unless toUpdate 680 | oldval = cols.map((col) -> oldObj[col]).join(",") 681 | newval = cols.map((col) -> updObj[col]).join(",") 682 | delete cls[oldval][updObj.id] 683 | delete cls[oldval] if Object.keys(cls[oldval]).length is 0 684 | cls[newval] = {} unless cls[newval]? 685 | cls[newval][updObj.id] = Table.CLASS_EXISTING_VALUE 686 | 687 | # firing event (FOR PERFORMANCE, existing check @db._hooks runs before emitting) 688 | @db._hooks["upd"] and @db._emit "upd", @name, updObj, oldObj, updCols 689 | @db._hooks["upd:" + @name] and @db._emit "upd:" + @name, updObj, oldObj, updCols 690 | 691 | # update related objects 692 | for exTblName, referred of @_referreds 693 | cols = Object.keys referred 694 | updateObjs = {} 695 | if cols.length is 1 696 | relatedObjs = argObj[exTblName] or argObj[exTblName + "." + cols[0]] 697 | (updateObjs[cols[0]] = if Array.isArray relatedObjs then relatedObjs else [relatedObjs]) if relatedObjs 698 | else 699 | for col in cols 700 | relatedObjs = argObj[exTblName + "." + col] 701 | (updateObjs[col] = if Array.isArray relatedObjs then relatedObjs else [relatedObjs]) if relatedObjs 702 | 703 | for col, relatedObjs of updateObjs 704 | # related objects with id 705 | idhash = {} 706 | for relatedObj in relatedObjs 707 | idhash[relatedObj.id] = relatedObj if relatedObj.id 708 | 709 | query = {} 710 | query[col + "_id"] = updObj.id 711 | exTable = @db.table(exTblName) 712 | oldIds = exTable.find(query, select: "id") 713 | 714 | # delte related objects in past unless options.append 715 | unless options.append 716 | exTable.del oldId for oldId in oldIds when not idhash[oldId] 717 | 718 | # update related objects if id exists 719 | exTable.upd idhash[oldId] for oldId in oldIds when idhash[oldId] 720 | 721 | # insert new related objects if id is not set 722 | for relatedObj in relatedObjs 723 | continue if relatedObj.id 724 | relatedObj[col + "_id"] = updObj.id 725 | exTable.ins relatedObj 726 | 727 | @db.save() if @db._autosave 728 | updObj 729 | 730 | ### 731 | # Table#upd() 732 | ### 733 | find : (query, options = {}, _priv = {}) -> 734 | report = Table._buildReportObj(options.explain) 735 | keys = @_indexes.id 736 | query = (if (_priv.normalized) then query else Table._normalizeQuery(query, @_rels)) 737 | if query 738 | keys = cup(query.map((condsList) -> 739 | ks = null 740 | Object.keys(condsList).forEach ((column) -> 741 | ks = cup(condsList[column].map((cond) -> 742 | localKeys = (if ks then ks.slice() else null) 743 | Object.keys(cond).forEach ((condType) -> 744 | localKeys = @_optSearch(column, condType, cond[condType], localKeys, report) 745 | return 746 | ), this 747 | localKeys 748 | , this)) 749 | return 750 | ), this 751 | ks 752 | , this)) 753 | else report.searches.push searchType: "none" if report 754 | 755 | # join tables 756 | joins = null 757 | joinCols = null 758 | if options.join 759 | joinInfos = @_getJoinInfos(options.join) 760 | joins = {} # key: id of the main object, value: joining_name => data(array) to join 761 | joinCols = [] 762 | reqCols = [] 763 | 764 | # join 1:N-related tables 765 | for info in joinInfos.N 766 | report and Table._reportSubQuery(report, info, "1:N") 767 | idcol = info.col 768 | name = info.name 769 | tblObj = @db.table(info.tbl) 770 | joinCols.push name 771 | reqCols.push name if info.req 772 | if info.emptyArray 773 | for id in keys 774 | joins[id] = {} unless joins[id] 775 | joins[id][name] = [] 776 | 777 | keys = keys.toArray() unless Array.isArray keys 778 | info.query = {} unless info.query 779 | info.query[idcol] = $in: keys 780 | for result in tblObj.find(info.query, info.options) 781 | orig_id = result[idcol] # id of the main object 782 | joins[orig_id] = {} unless joins[orig_id] 783 | joins[orig_id][name] = [] unless joins[orig_id][name] 784 | joins[orig_id][name].push result 785 | 786 | if info.offset? or info.limit? 787 | for id, value of joins 788 | arr = value[name] 789 | value[name] = Table._offsetLimit(arr, info.offset, info.limit) if arr 790 | 791 | if info.select 792 | if typeof info.select is "string" 793 | for id, value of joins 794 | if value[name]? 795 | value[name] = value[name].map (v) -> v[info.select] 796 | else 797 | (Array.isArray(info.select)) or err("typeof options.select must be one of string, null, array") 798 | for id, value of joins 799 | arr = value[name] 800 | if arr 801 | value[name] = value[name].map (v) -> 802 | ret = {} 803 | for col in info.select 804 | ret[col] = v[col] 805 | return ret 806 | 807 | # join N:1-related tables 808 | for info in joinInfos["1"] 809 | report and Table._reportSubQuery(report, info, "N:1") 810 | idcol = info.col 811 | name = info.name 812 | tblObj = @db.table(info.tbl) 813 | q = Table._normalizeQuery(info.query, @_rels) 814 | joinCols.push name 815 | reqCols.push name if info.req 816 | for id in keys 817 | exId = tblObj._survive(@_data[id][idcol], q, true) 818 | continue unless exId? 819 | joins[id] = {} unless joins[id] 820 | joins[id][name] = tblObj._data[exId] 821 | 822 | # actual joining 823 | keys = keys.filter (id) -> 824 | joinColObj = joins[id] 825 | joinColObj = {} unless joinColObj 826 | reqCols.every (col) -> 827 | joinColObj[col] 828 | 829 | keys = @_orderBy(keys, options.order, report) if options.order? 830 | keys = Table._offsetLimit(keys, options.offset, options.limit) if options.offset? or options.limit? 831 | res = @_select(keys, options.select, joins, joinCols) 832 | return res unless options.groupBy 833 | ret = {} 834 | keyColumn = (if options.groupBy is true then "id" else options.key) 835 | ret[item[keyColumn]] = item for item in res 836 | return ret 837 | 838 | JSRel.Table = Table 839 | Table::one = (query, options, _priv) -> 840 | query = id: query if typeof query is "number" or not isNaN(Number(query)) 841 | ret = @find(query, options, _priv) 842 | (if (ret.length) then ret[0] else null) 843 | 844 | Table::count = (query) -> 845 | return @_indexes.id.length unless query 846 | @find(query, 847 | select: "id" 848 | ).length 849 | 850 | Table::del = (arg, options) -> 851 | options or (options = {}) 852 | delList = undefined 853 | if typeof arg is "number" 854 | (@_data[arg]) or err("id", arg, "is not found in table", @name) 855 | delList = [@_data[arg]] 856 | else 857 | delList = @find(arg) 858 | delList.forEach ((obj) -> 859 | Object.keys(@_indexes).forEach ((idxName) -> 860 | list = @_indexes[idxName] 861 | keys = list.keys(obj.id) 862 | (keys?) or err("invalid keys") 863 | bool = keys.some((key) -> 864 | if obj.id is list[key] 865 | list.remove key 866 | true 867 | ) 868 | (bool) or err("index was not deleted.") 869 | return 870 | ), this 871 | Object.keys(@_classes).forEach ((columns) -> 872 | cls = @_classes[columns] 873 | cols = columns.split(",") 874 | val = cols.map((col) -> 875 | obj[col] 876 | ) 877 | (cls[val][obj.id] is Table.CLASS_EXISTING_VALUE) or err("deleting object is not in classes.", quo(obj.id), "in table", quo(@name)) 878 | delete cls[val][obj.id] 879 | 880 | delete cls[val] if Object.keys(cls[val]).length is 0 881 | return 882 | ), this 883 | delete @_data[obj.id] 884 | 885 | @db._emit "del", @name, obj 886 | @db._emit "del:" + @name, obj 887 | Object.keys(@_referreds).forEach ((exTable) -> 888 | query = {} 889 | info = @_referreds[exTable] 890 | Object.keys(info).forEach ((colName) -> 891 | required = info[colName] 892 | query[colName + "_id"] = obj.id 893 | if required 894 | @db.table(exTable).del query, 895 | sub: true 896 | 897 | else 898 | upd = {} 899 | upd[colName + "_id"] = null 900 | @db.table(exTable).find(query).forEach ((o) -> 901 | upd.id = o.id 902 | @db.table(exTable).upd upd, 903 | sub: true 904 | 905 | return 906 | ), this 907 | return 908 | ), this 909 | return 910 | ), this 911 | return 912 | ), this 913 | @db.save() if @db._autosave 914 | this 915 | 916 | Table::_getNewId = -> 917 | len = @_indexes.id.length 918 | return 1 unless len 919 | @_indexes.id[len - 1] + 1 920 | 921 | Table::_optSearch = (col, condType, value, ids, report) -> 922 | (@_colInfos[col]) or err("unknown column", quo(col)) 923 | lists = 924 | index: @_indexes[col] 925 | classes: @_classes[col] 926 | noIndex: ids 927 | 928 | searchType = undefined 929 | if (ids and ids.length < Table.NOINDEX_MIN_LIMIT) or (not lists.index and not lists.classes) or condType is "like" 930 | searchType = "noIndex" 931 | else 932 | switch condType 933 | when "equal", "$in" 934 | searchType = (if lists.classes then "classes" else "index") 935 | when "gt", "ge", "lt", "le" 936 | searchType = (if lists.index then "index" else "classes") 937 | when "like$" 938 | searchType = (if lists.index then "index" else "noIndex") 939 | else 940 | err "undefined condition", quo(condType) 941 | result = Queries[searchType][condType].call(this, col, value, lists[searchType] or @_indexes.id) 942 | ret = (if (searchType is "noIndex" or not ids) then result else conjunction(ids, result)) 943 | if report 944 | report.searches.push 945 | searchType: searchType 946 | condition: condType 947 | column: col 948 | value: value 949 | count: result.length 950 | before: (if ids then ids.length else null) 951 | after: ret.length 952 | 953 | ret 954 | 955 | Table::_idxSearch = (list, obj, fn, nocopy) -> 956 | ob = if nocopy then obj else copy obj 957 | ob.id = Table.ID_TEMP unless ob.id? 958 | @_data[Table.ID_TEMP] = ob 959 | ret = fn.call(@, ob, @_data) 960 | delete @_data[Table.ID_TEMP] 961 | return ret 962 | 963 | Table::_idxSearchByValue = (list, col, value, fn) -> 964 | obj = {} 965 | obj[col] = value 966 | @_idxSearch list, obj, fn, true 967 | 968 | Table::_convertRelObj = (obj) -> 969 | Object.keys(@_rels).forEach (col) -> 970 | #return if obj[col + "_id"]? 971 | if obj[col] and obj[col].id? 972 | obj[col + "_id"] = obj[col].id 973 | delete obj[col] 974 | return 975 | 976 | obj 977 | 978 | Table::_cast = (colName, obj) -> 979 | val = obj[colName] 980 | return if Table.AUTO_ADDED_COLUMNS[colName] and not val? 981 | colInfo = @_colInfos[colName] 982 | return if typeof val is Table.TYPES[colInfo.type] 983 | if not colInfo.required and not val? 984 | val = colInfo._default 985 | else 986 | (val?) or err("column", "\"" + colName + "\"", "is required.") 987 | switch colInfo.type 988 | when Table._NUM 989 | val = Number(val) 990 | (not isNaN(val)) or err(quo(colName), ":", quo(obj[colName]), "is not a valid number.") 991 | when Table._BOOL 992 | val = !!val 993 | when Table._STR 994 | (typeof val.toString is "function") or err("cannot convert", val, "to string") 995 | val = val.toString() 996 | obj[colName] = val 997 | obj 998 | 999 | Table::_checkUnique = (idxName, obj) -> 1000 | list = @_indexes[idxName] 1001 | return unless list._unique 1002 | @_idxSearch list, obj, (tmpObj, data) -> 1003 | (not (list.key(tmpObj.id)?)) or err("duplicated entry :", idxName.split(",").map((col) -> 1004 | obj[col] 1005 | ).join(","), "in", idxName) 1006 | return 1007 | 1008 | return 1009 | 1010 | Table::_compress = -> 1011 | cData = Table._compressData(@_colInfos, @_data, @_indexes, @_idxKeys) 1012 | cClasses = Table._compressClasses(@_classes) 1013 | cRels = Table._compressRels(@_rels, @_referreds) 1014 | [ 1015 | cData 1016 | cClasses 1017 | cRels 1018 | ] 1019 | 1020 | Table._compressData = (colInfos, data, indexes, idxKeys) -> 1021 | cols = [] 1022 | compressedColInfos = Object.keys(colInfos).map((col) -> 1023 | colInfo = colInfos[col] 1024 | cols.push colInfo.name 1025 | Table.COLKEYS.map (key) -> 1026 | colInfo[key] 1027 | 1028 | , this) 1029 | boolTypes = cols.reduce((ret, col) -> 1030 | ret[col] = 1 if colInfos[col].type is Table._BOOL 1031 | ret 1032 | , {}) 1033 | compressedData = Object.keys(data).map((id) -> 1034 | obj = data[id] 1035 | cols.map (col) -> 1036 | (if (boolTypes[col]) then (if obj[col] then 1 else 0) else obj[col]) 1037 | 1038 | , this) 1039 | compressedIndexes = Object.keys(indexes).map((idxName) -> 1040 | list = indexes[idxName] 1041 | [ 1042 | idxName 1043 | list._unique 1044 | list.toArray() 1045 | ] 1046 | ) 1047 | [ 1048 | compressedColInfos 1049 | compressedData 1050 | compressedIndexes 1051 | ] 1052 | 1053 | Table._decompressData = (cdata) -> 1054 | infos = cdata[0] 1055 | darr = cdata[1] 1056 | cIndexes = cdata[2] 1057 | colInfos = {} 1058 | cols = infos.map((info, k) -> 1059 | obj = {} 1060 | Table.COLKEYS.forEach (colkey, n) -> 1061 | obj[colkey] = info[n] 1062 | return 1063 | 1064 | col = obj.name 1065 | colInfos[col] = obj 1066 | col 1067 | ) 1068 | boolTypes = cols.reduce((ret, col) -> 1069 | ret[col] = 1 if colInfos[col].type is Table._BOOL 1070 | ret 1071 | , {}) 1072 | data = darr.reduce((ret, d, k) -> 1073 | record = {} 1074 | cols.forEach (col, k) -> 1075 | record[col] = (if boolTypes[col] then !!d[k] else d[k]) 1076 | return 1077 | 1078 | ret[record.id] = record 1079 | ret 1080 | , {}) 1081 | indexes = cIndexes.reduce((indexes, nameUniqArr) -> 1082 | idxName = nameUniqArr[0] 1083 | columns = idxName.split(",") 1084 | uniq = nameUniqArr[1] 1085 | types = columns.map((col) -> 1086 | colInfos[col].type 1087 | ) 1088 | arr = nameUniqArr[2] 1089 | indexes[idxName] = Table._getIndex(columns, uniq, types, arr, data) 1090 | indexes 1091 | , {}) 1092 | idxKeys = Table._getIdxKeys(indexes) 1093 | [ 1094 | colInfos 1095 | data 1096 | indexes 1097 | idxKeys 1098 | ] 1099 | 1100 | Table._compressClasses = (classes) -> 1101 | Object.keys(classes).map (col) -> 1102 | cls = classes[col] 1103 | cols = cls.cols 1104 | delete cls.cols 1105 | 1106 | vals = Object.keys(cls).map((val) -> 1107 | [ 1108 | val 1109 | Object.keys(cls[val]).map((v) -> 1110 | Number v 1111 | ) 1112 | ] 1113 | ) 1114 | cls.cols = cols 1115 | [ 1116 | col 1117 | vals 1118 | ] 1119 | 1120 | 1121 | Table._decompressClasses = (cClasses) -> 1122 | cClasses.reduce ((classes, colvals) -> 1123 | col = colvals[0] 1124 | classes[col] = colvals[1].reduce((cls, valkeys) -> 1125 | val = valkeys[0] 1126 | cls[val] = valkeys[1].reduce((idhash, id) -> 1127 | idhash[id] = 1 1128 | idhash 1129 | , {}) 1130 | cls 1131 | , {}) 1132 | classes[col].cols = col.split(",") 1133 | classes 1134 | ), {} 1135 | 1136 | Table._compressRels = (rels, referreds) -> 1137 | [ 1138 | rels 1139 | referreds 1140 | ] 1141 | 1142 | Table._decompressRels = (c) -> 1143 | c 1144 | 1145 | Table._columnToSQL = (info, colConverts) -> 1146 | colType = Table.TYPE_SQLS[info.sqltype] 1147 | name = (if (info.name of colConverts) then colConverts[info.name] else info.name) 1148 | stmt = [ 1149 | bq(name) 1150 | colType 1151 | ] 1152 | stmt.push "NOT NULL" if info.required 1153 | if info._default? 1154 | defa = (if (info.type is Table._BOOL) then (if info._default then 1 else 0) else (if (info.type is Table._STR) then quo(info._default) else info._default)) 1155 | stmt.push "DEFAULT", defa 1156 | stmt.push "PRIMARY KEY AUTO_INCREMENT" if name is "id" 1157 | stmt.join " " 1158 | 1159 | Table._idxToSQL = (name, list, colConverts) -> 1160 | return if name is "id" 1161 | name = colConverts[name] if name of colConverts 1162 | uniq = (if (list._unique) then "UNIQUE " else "") 1163 | [ 1164 | uniq + "INDEX" 1165 | "(" + name + ")" 1166 | ].join " " 1167 | 1168 | Table::_toDropSQL = (options) -> 1169 | ifExist = true 1170 | "DROP TABLE " + ((if ifExist then "IF EXISTS " else "")) + bq(@name) + ";" 1171 | 1172 | Table::_toCreateSQL = (options) -> 1173 | options or (options = {}) 1174 | colConverts = options.columns or {} 1175 | delete colConverts.id 1176 | 1177 | substmts = @columns.map((col) -> 1178 | Table._columnToSQL @_colInfos[col], colConverts 1179 | , this) 1180 | Object.keys(@_indexes).forEach ((idxName) -> 1181 | idxSQL = Table._idxToSQL(idxName, @_indexes[idxName], colConverts) 1182 | substmts.push idxSQL if idxSQL 1183 | return 1184 | ), this 1185 | Object.keys(@_rels).forEach ((fkey) -> 1186 | exTbl = @_rels[fkey] 1187 | fkey_disp = (if (fkey of colConverts) then colConverts[fkey] else (fkey + "_id")) 1188 | stmt = "FOREIGN KEY (" + fkey_disp + ") REFERENCES " + exTbl + "(id)" 1189 | required = @db.table(exTbl)._referreds[@name][fkey] 1190 | if required 1191 | stmt += " ON UPDATE CASCADE ON DELETE CASCADE" 1192 | else 1193 | stmt += " ON UPDATE NO ACTION ON DELETE SET NULL" 1194 | substmts.push stmt 1195 | return 1196 | ), this 1197 | "CREATE TABLE " + bq(@name) + "(" + substmts.join(",") + ")" + ((if options.type is "mysql" and options.engine then " ENGINE=" + options.engine else "")) + ";" 1198 | 1199 | Table::_toInsertSQL = (options) -> 1200 | options or (options = {}) 1201 | colConverts = options.columns or {} 1202 | delete colConverts.id 1203 | 1204 | colInfos = @_colInfos 1205 | boolTypes = @columns.reduce((ret, col) -> 1206 | ret[col] = 1 if colInfos[col].type is Table._BOOL 1207 | ret 1208 | , {}) 1209 | columnNames = @columns.map((name) -> 1210 | (if (name of colConverts) then colConverts[name] else name) 1211 | ) 1212 | valConverts = options.values or {} 1213 | Object.keys(valConverts).forEach (col) -> 1214 | delete valConverts[col] unless typeof valConverts[col] is "function" 1215 | return 1216 | 1217 | stmt = [ 1218 | "INSERT INTO " 1219 | bq(@name) 1220 | "(" 1221 | columnNames.map(bq).join(",") 1222 | ") VALUES " 1223 | ].join(" ") 1224 | ret = [] 1225 | cur = undefined 1226 | i = 0 1227 | l = @_indexes.id.length 1228 | 1229 | while i < l 1230 | id = @_indexes.id[i] 1231 | record = @_data[id] 1232 | vals = @columns.map((col) -> 1233 | v = record[col] 1234 | v = valConverts[col](v) if col of valConverts 1235 | (if boolTypes[col] then (if v then 1 else 0) else (if (typeof v is "number") then v else quo(v))) 1236 | ).join(",") 1237 | if i % 1000 is 0 1238 | ret.push cur if cur 1239 | cur = 1240 | st: stmt 1241 | ar: [] 1242 | cur.ar.push "(" + vals + ")" 1243 | i++ 1244 | ret.push cur if cur and cur.ar.length 1245 | ret.map((cur) -> 1246 | cur.st + cur.ar.join(",\n") + ";\n" 1247 | ).join "\n" 1248 | 1249 | Table::_parseRaw = (info) -> 1250 | indexes = info._indexes 1251 | delete info._indexes 1252 | 1253 | Object.keys(info).forEach ((k) -> 1254 | this[k] = info[k] 1255 | return 1256 | ), this 1257 | Object.keys(indexes).forEach ((idxName) -> 1258 | ids = indexes[idxName] 1259 | isUniq = ids._unique 1260 | @_setIndex idxName.split(","), isUniq, Array::slice.call(ids) 1261 | return 1262 | ), this 1263 | this 1264 | 1265 | Table::_parseCompressed = (c) -> 1266 | colInfoDataIdxesKeys = Table._decompressData(c[0]) 1267 | @_colInfos = colInfoDataIdxesKeys[0] 1268 | @_data = colInfoDataIdxesKeys[1] 1269 | @_indexes = colInfoDataIdxesKeys[2] 1270 | @_idxKeys = colInfoDataIdxesKeys[3] 1271 | @_classes = Table._decompressClasses(c[1]) 1272 | relsReferreds = Table._decompressRels(c[2]) 1273 | @_rels = relsReferreds[0] 1274 | @_referreds = relsReferreds[1] 1275 | return 1276 | 1277 | Table::_parseSchema = (colData) -> 1278 | colData = copy(colData) 1279 | tblName = @name 1280 | for invalidColumn in Table.INVALID_COLUMNS 1281 | err(invalidColumn, "is not allowed for a column name") if colData[invalidColumn]? 1282 | 1283 | metaInfos = Table.COLUMN_META_KEYS.reduce((ret, k) -> 1284 | ret[k] = arrayize(colData[k], true) 1285 | delete colData[k] 1286 | 1287 | ret 1288 | , {}) 1289 | colData.id = 1 1290 | colData.upd_at = 1 1291 | colData.ins_at = 1 1292 | metaInfos.$uniques.unshift "id" 1293 | metaInfos.$indexes.unshift "upd_at", "ins_at" 1294 | columnNames = Object.keys(colData) 1295 | columnNames.forEach (col) -> 1296 | (not (col.match(/[,.`"']/)?)) or err("comma, dot and quotations cannot be included in a column name.") 1297 | return 1298 | 1299 | (columnNames.length > 3) or err("table", quo(tblName), "must contain at least one column.") 1300 | columnNames.forEach ((colName) -> 1301 | parsed = @__parseColumn(colName, colData[colName]) 1302 | (not (@_colInfos[parsed.name]?)) or err(quo(parsed.name), "is already registered.") 1303 | @_colInfos[parsed.name] = parsed 1304 | return 1305 | ), this 1306 | Object.keys(@_colInfos).forEach ((colName) -> 1307 | colInfo = @_colInfos[colName] 1308 | exTblName = colInfo.rel 1309 | return unless exTblName 1310 | (colName.slice(-3) is "_id") or err("Relation columns must end with \"_id\".") 1311 | exTable = @db.table(exTblName) 1312 | (exTable) or err("Invalid relation: ", quo(exTblName), "is an undefined table in", quo(tblName)) 1313 | metaInfos.$indexes.push colName 1314 | col = colName.slice(0, -3) 1315 | @_rels[col] = exTblName 1316 | exTable._referreds[tblName] = {} unless exTable._referreds[tblName] 1317 | exTable._referreds[tblName][col] = @_colInfos[colName].required 1318 | return 1319 | ), this 1320 | Object.keys(metaInfos).forEach ((k) -> 1321 | metaInfos[k] = @_normalizeIndexes(metaInfos[k]) 1322 | return 1323 | ), this 1324 | metaInfos.$indexes.forEach ((cols) -> 1325 | @_setIndex cols, false 1326 | return 1327 | ), this 1328 | metaInfos.$uniques.forEach ((cols) -> 1329 | @_setIndex cols, true 1330 | return 1331 | ), this 1332 | metaInfos.$classes.forEach ((cols) -> 1333 | @_setClass cols 1334 | return 1335 | ), this 1336 | @_idxKeys = Table._getIdxKeys(@_indexes) 1337 | return 1338 | 1339 | Table::_setIndex = (cols, isUniq, ids) -> 1340 | strCols = [] 1341 | types = cols.map((col) -> 1342 | ret = @_colInfos[col].type 1343 | strCols.push col if ret is Table._STR 1344 | ret 1345 | , this) 1346 | len = strCols.length 1347 | strCols.forEach ((col) -> 1348 | @_colInfos[col].sqltype = (if (len > 1) then Table._CHR2 else Table._CHRS) 1349 | return 1350 | ), this 1351 | idxName = cols.join(",") 1352 | return if @_indexes[idxName]? 1353 | @_indexes[idxName] = Table._getIndex(cols, isUniq, types, ids, @_data) 1354 | return 1355 | 1356 | Table._getIndex = (cols, isUniq, types, ids, data) -> 1357 | SortedList.create 1358 | compare: generateCompare(types, cols, data) 1359 | unique: !!isUniq 1360 | resume: true 1361 | , ids 1362 | 1363 | Table._getIdxKeys = (indexes) -> 1364 | Object.keys(indexes).reduce ((ret, idxName) -> 1365 | idxName.split(",").forEach (col) -> 1366 | ret[col] = [] unless ret[col] 1367 | ret[col].push idxName 1368 | return 1369 | 1370 | ret 1371 | ), {} 1372 | 1373 | Table::_setClass = (cols) -> 1374 | idxname = cols.join(",") 1375 | return if @_classes[idxname]? 1376 | cols.forEach ((col) -> 1377 | (@_colInfos[col].type isnt Table._STR) or err("Cannot set class index to string columns", quo(col)) 1378 | return 1379 | ), this 1380 | @_classes[idxname] = cols: cols 1381 | return 1382 | 1383 | ### 1384 | # parse join options from find() 1385 | # returns canonical information of join (joinInfos) 1386 | # joinInfos: 1387 | # 1: array of joinInfo (N:1 relation) 1388 | # N: array of joinInfo (1:N relation) 1389 | # 1390 | # joinInfo: 1391 | # name: 1392 | # req: 1393 | # emptyArray: 1394 | # limit: 1395 | # offset: 1396 | # select: 1397 | # query: the first argument for find() 1398 | # options: the second argument for find() 1399 | ### 1400 | Table::_getJoinInfos = (joinOptions) -> 1401 | if joinOptions is true 1402 | joinOptions = {} 1403 | joinOptions[col] = true for col, tblname of @_rels 1404 | 1405 | else if typeof joinOptions is "string" 1406 | k = joinOptions 1407 | joinOptions = {} 1408 | joinOptions[k] = true 1409 | joinInfos = 1410 | 1: [] 1411 | N: [] 1412 | 1413 | for tbl_col, options of joinOptions 1414 | joinInfo = @_resolveTableColumn(tbl_col, options) # tbl, col and reltype is set 1415 | joinInfo.options = {} 1416 | 1417 | if typeof options is "object" 1418 | joinInfo.name = if options.as then options.as else tbl_col 1419 | joinInfo.req = !options.outer 1420 | joinInfo.emptyArray = true if options.outer is "array" 1421 | delete options.as 1422 | delete options.outer 1423 | delete options.explain 1424 | 1425 | for op in [ "limit", "offset", "select"] 1426 | if options[op]? 1427 | joinInfo[op] = options[op] 1428 | delete options[op] 1429 | 1430 | for op in ["order", "join"] 1431 | if options[op]? 1432 | joinInfo.options[op] = options[op] 1433 | delete options[op] 1434 | 1435 | query = options 1436 | if options.where 1437 | query[k] = v for k, v of options.where 1438 | delete query.where 1439 | joinInfo.query = query 1440 | else 1441 | joinInfo.name = tbl_col 1442 | joinInfo.req = true 1443 | 1444 | joinInfos[joinInfo.reltype].push joinInfo 1445 | return joinInfos 1446 | 1447 | ### 1448 | # resolve table name and column name from given join option 1449 | # tbl_col : table name or column name of related table. 1450 | # Format of "tablename.columename" is allowed to specify both precisely 1451 | # returns tableColumn (tbl: table, col: column, reltype : "1" or "N", "1" means N:1 relation, "N" means 1:N (or N:M) relation) 1452 | ### 1453 | Table::_resolveTableColumn = (tbl_col, options) -> 1454 | tbl_col = tbl_col.split(".") 1455 | len = tbl_col.length 1456 | (len <= 2) or err(quo(tbl_col), "is invalid expression", quo(k)) 1457 | 1458 | if len is 1 1459 | if @_rels[tbl_col[0]] # if given tbl_col is one of the name of N:1-related column 1460 | col = tbl_col[0] 1461 | tableColumn = 1462 | col : col + "_id" 1463 | tbl : @_rels[col] 1464 | reltype : "1" 1465 | 1466 | else # 1:N or N:M 1467 | tbl = tbl_col[0] 1468 | referred = @_referreds[tbl] 1469 | 1470 | if referred # 1:N 1471 | refCols = Object.keys(referred) 1472 | (refCols.length is 1) or err("table", quo(tbl), "refers", quo(@name), "multiply. You can specify table and column like", quo("table_name.column_name")) 1473 | tableColumn = 1474 | tbl : tbl 1475 | col : refCols[0] + "_id" 1476 | reltype : "N" 1477 | 1478 | else # N:M via "via" 1479 | (typeof options is "object" and options.via?) or err("table", quo(tbl), "is not referring table", quo(@name)) 1480 | tableColumn = @_resolveTableColumn(options.via) # first, joins 1:N table 1481 | delete options.via 1482 | 1483 | # modify joinOptions so as to nest sub-joining info 1484 | subJoinInfo = {} 1485 | for option, value of options 1486 | continue if option is "as" 1487 | continue if option is "outer" 1488 | subJoinInfo[option] = value 1489 | delete options[option] 1490 | 1491 | options.join = {} unless options.join 1492 | options.join[tbl] = subJoinInfo 1493 | options.select = tbl 1494 | 1495 | else # 1:N-related table and column, expressed in "tablename.columnname" 1496 | [tbl, col] = tbl_col 1497 | referred = @_referreds[tbl] 1498 | refCols = Object.keys(referred) 1499 | (refCols) or err("table", quo(tbl), "is not referring table", quo(@name)) 1500 | (refCols.indexOf(col) >= 0) or err("table", quo(tbl), "does not have a column", quo(col)) 1501 | tableColumn = 1502 | tbl : tbl 1503 | col : col + "_id" 1504 | reltype : "N" 1505 | return tableColumn 1506 | 1507 | Table::_normalizeIndexes = (arr) -> 1508 | arr.map ((def) -> 1509 | def = arrayize(def) 1510 | def.map ((col) -> 1511 | col = col + "_id" if @_rels[col] 1512 | (@_colInfos[col] isnt `undefined`) or err(quo(col), "is unregistered column. in", quo(@name)) 1513 | col 1514 | ), this 1515 | ), this 1516 | 1517 | Table::__parseColumn = (colName, columnOption) -> 1518 | colObj = 1519 | name: colName 1520 | type: Table._STR 1521 | sqltype: Table._STR 1522 | required: false 1523 | _default: null 1524 | rel: false 1525 | 1526 | switch columnOption 1527 | when true 1528 | colObj.required = true 1529 | when "str", "text", false 1530 | break 1531 | when "req" 1532 | colObj.type = Table._STR 1533 | colObj.sqltype = Table._CHRS 1534 | colObj.required = true 1535 | when "not", "chars", "" 1536 | colObj.type = Table._STR 1537 | colObj.sqltype = Table._CHRS 1538 | when 1 1539 | colObj.type = Table._NUM 1540 | colObj.sqltype = Table._INT 1541 | colObj.required = true 1542 | when "int", 0 1543 | colObj.type = Table._NUM 1544 | colObj.sqltype = Table._INT 1545 | when "num", "float" 1546 | colObj.type = colObj.sqltype = Table._NUM 1547 | when 1.1 1548 | colObj.type = colObj.sqltype = Table._NUM 1549 | when 0.1 1550 | colObj.type = colObj.sqltype = Table._NUM 1551 | colObj.required = true 1552 | when "on" 1553 | colObj.type = colObj.sqltype = Table._BOOL 1554 | colObj._default = true 1555 | when "bool", "off" 1556 | colObj.type = colObj.sqltype = Table._BOOL 1557 | colObj._default = false 1558 | else 1559 | columnOption = type: columnOption if typeof columnOption is "string" 1560 | (columnOption and columnOption.type) or err("invalid column description.") 1561 | switch columnOption.type 1562 | when "text", "string", "str" 1563 | colObj.type = colObj.sqltype = Table._STR 1564 | when "double", "float", "number", "num" 1565 | colObj.type = colObj.sqltype = Table._NUM 1566 | when "boolean", "bool" 1567 | colObj.type = colObj.sqltype = Table._BOOL 1568 | when "int" 1569 | colObj.type = Table._NUM 1570 | colObj.sqltype = Table._INT 1571 | when "chars" 1572 | colObj.type = Table._STR 1573 | colObj.sqltype = Table._CHRS 1574 | else 1575 | colObj.name += "_id" 1576 | colObj.type = Table._NUM 1577 | colObj.sqltype = Table._INT 1578 | colObj.rel = columnOption.type 1579 | columnOption.required = true if columnOption.required is `undefined` 1580 | if columnOption._default? 1581 | (typeof columnOption._default is Table.TYPES[colObj.type]) or err("type of the default value", columnOption._default, "does not match", Table.TYPES[colObj.type], "in", colObj.name) 1582 | colObj._default = columnOption._default 1583 | colObj.sqltype = Table._CHRS if colObj.sqltype is Table._STR 1584 | colObj.required = !!columnOption.required if columnOption.required 1585 | colObj 1586 | 1587 | Table::_orderBy = (keys, order, report) -> 1588 | return keys unless order 1589 | orders = objectize(order, "asc") 1590 | Object.keys(orders).reverse().forEach ((k) -> 1591 | orderType = orders[k] 1592 | if @_indexes[k] and keys.length * 4 > @_indexes.id.length 1593 | if report 1594 | report.orders.push 1595 | column: k 1596 | type: orderType 1597 | method: "index" 1598 | 1599 | idx = @_indexes[k] 1600 | keys = conjunction(idx, keys) 1601 | keys = keys.reverse() if orderType is "desc" 1602 | else 1603 | keys = keys.slice().sort(generateCompare(@_colInfos[k].type, k, @_data)) 1604 | if report 1605 | report.orders.push 1606 | column: k 1607 | type: orderType 1608 | method: "sort" 1609 | 1610 | keys = keys.reverse() if orderType is "desc" 1611 | return 1612 | ), this 1613 | keys 1614 | 1615 | Table::_select = (keys, cols, joins, joinCols) -> 1616 | if typeof cols is "string" 1617 | if cols is "id" 1618 | return (if (keys.toArray) then keys.toArray() else keys) if keys.length is 0 or typeof keys[0] is "number" 1619 | return keys.map Number 1620 | if joinCols and joinCols.indexOf(cols) >= 0 1621 | return keys.map((id) -> 1622 | joins[id][cols] 1623 | , this) 1624 | (@_colInfos[cols]) or err("column", quo(cols), "is not found in table", quo(@name)) 1625 | return keys.map((id) -> 1626 | @_data[id][cols] 1627 | , this) 1628 | unless cols? 1629 | ret = keys.map((id) -> 1630 | copy @_data[id] 1631 | , this) 1632 | if joins and joinCols and joinCols.length 1633 | ret.forEach (obj) -> 1634 | joinCols.forEach (col) -> 1635 | obj[col] = (if (not (joins[obj.id]?)) then null else joins[obj.id][col]) 1636 | return 1637 | return 1638 | return ret 1639 | err("typeof options.select", cols, "must be string, null, or array") unless Array.isArray(cols) 1640 | inputCols = cols 1641 | _joinCols = [] 1642 | cols = [] 1643 | inputCols.forEach ((col) -> 1644 | if joins and joinCols and joinCols.indexOf(col) >= 0 1645 | _joinCols.push col 1646 | else if @_colInfos[col] 1647 | cols.push col 1648 | else 1649 | err "column", quo(col), "is not found in table", quo(@name) 1650 | return 1651 | ), this 1652 | ret = keys.map((id) -> 1653 | ob = {} 1654 | cols.forEach ((col) -> 1655 | ob[col] = @_data[id][col] 1656 | return 1657 | ), this 1658 | ob 1659 | , this) 1660 | if joins and _joinCols.length 1661 | ret.forEach (obj) -> 1662 | _joinCols.forEach (col) -> 1663 | obj[col] = joins[obj.id][col] 1664 | return 1665 | 1666 | return 1667 | 1668 | ret 1669 | 1670 | Table::_survive = (id, query, normalized) -> 1671 | return id unless query 1672 | that = this 1673 | query = (if (normalized) then query else Table._normalizeQuery(query)) 1674 | (if query.some((condsList) -> 1675 | Object.keys(condsList).every (column) -> 1676 | condsList[column].some (cond) -> 1677 | Object.keys(cond).every (condType) -> 1678 | Queries.noIndex[condType].call(that, column, cond[condType], [id]).length 1679 | 1680 | 1681 | 1682 | ) then id else null) 1683 | 1684 | Table._normalizeQuery = (query, rels) -> 1685 | return null if not query or not Object.keys(query).length 1686 | arrayize(query).map (condsList) -> 1687 | Object.keys(condsList).reduce ((ret, column) -> 1688 | conds = condsList[column] 1689 | if rels[column] 1690 | conds = condsList[column].id 1691 | column += "_id" 1692 | ret[column] = arrayize(conds).map((cond) -> 1693 | (if (cond is null) then equal: null else (if (typeof cond is "object") then cond else equal: cond)) 1694 | ) 1695 | ret 1696 | ), {} 1697 | 1698 | 1699 | Table._reportSubQuery = (report, info, reltype) -> 1700 | subreport = 1701 | reltype: reltype 1702 | table: info.tbl 1703 | join_column: info.col 1704 | name: info.name 1705 | outer: not info.req 1706 | emptyArray: !!info.emptyArray 1707 | 1708 | info.options.explain = subreport 1709 | report.subqueries.push subreport 1710 | return 1711 | 1712 | Table._offsetLimit = (keys, offset, limit) -> 1713 | return keys if not offset? and not limit? 1714 | offset = offset or 0 1715 | end = (if limit then (limit + offset) else keys.length) 1716 | keys.slice offset, end 1717 | 1718 | Table._buildReportObj = (obj) -> 1719 | return null unless obj 1720 | obj.searches = [] unless obj.searches 1721 | obj.subqueries = [] unless obj.subqueries 1722 | obj.orders = [] unless obj.orders 1723 | obj 1724 | 1725 | Object.keys(Table::).forEach (name) -> 1726 | return if name.charAt(0) is "_" 1727 | method = Table::[name] 1728 | return unless typeof method is "function" 1729 | JSRel::[name] = (args...)-> 1730 | tblName = args.shift() 1731 | tbl = @table(tblName) 1732 | (tbl) or err("invalid table name", quo(tblName)) 1733 | tbl[name].apply tbl, args 1734 | 1735 | return 1736 | 1737 | Queries = 1738 | index: {} 1739 | classes: {} 1740 | noIndex: {} 1741 | 1742 | Queries.index.equal = (col, value, list) -> 1743 | @_idxSearchByValue list, col, value, (obj, data) -> 1744 | keys = list.keys(obj.id) 1745 | return unless keys then [] else keys.map (k) -> list[k] 1746 | 1747 | 1748 | Queries.index.like$ = (col, value, list) -> 1749 | @_idxSearchByValue list, col, value, ((obj, data) -> 1750 | pos = list.bsearch(obj.id) 1751 | key = list.key(obj.id, pos) 1752 | results = [] 1753 | i = (if (key?) then key else pos + 1) 1754 | len = list.length 1755 | cur = undefined 1756 | v = undefined 1757 | included = false 1758 | loop 1759 | cur = data[list[i]] 1760 | v = cur[col] 1761 | 1762 | if v.indexOf(value) is 0 1763 | included = true 1764 | results.push cur.id 1765 | else 1766 | included = false 1767 | break unless ++i < len and (v <= value or included) 1768 | results 1769 | ), this 1770 | 1771 | Queries.index.gt = (col, value, list) -> 1772 | return [] unless list.length 1773 | @_idxSearchByValue list, col, value, (obj, data) -> 1774 | i = list.bsearch(obj.id) + 1 1775 | len = list.length 1776 | cur = undefined 1777 | v = undefined 1778 | loop 1779 | cur = data[list[i]] 1780 | v = cur[col] 1781 | break unless ++i < len and v <= value 1782 | list.slice i 1783 | 1784 | 1785 | Queries.index.ge = (col, value, list) -> 1786 | return [] unless list.length 1787 | @_idxSearchByValue list, col, value, (obj, data) -> 1788 | pos = list.bsearch(obj.id) 1789 | key = list.key(obj.id, pos) 1790 | list.slice (if (key?) then key else pos + 1) 1791 | 1792 | 1793 | Queries.index.lt = (col, value, list) -> 1794 | return [] unless list.length 1795 | @_idxSearchByValue list, col, value, (obj, data) -> 1796 | pos = list.bsearch(obj.id) 1797 | key = list.key(obj.id, pos) 1798 | list.slice 0, (if (key?) then key else pos + 1) 1799 | 1800 | 1801 | Queries.index.le = (col, value, list) -> 1802 | return [] unless list.length 1803 | @_idxSearchByValue list, col, value, (obj, data) -> 1804 | i = list.bsearch(obj.id) + 1 1805 | len = list.length 1806 | cur = undefined 1807 | v = undefined 1808 | loop 1809 | cur = data[list[i]] 1810 | v = cur[col] 1811 | break unless ++i < len and v <= value 1812 | list.slice 0, i 1813 | 1814 | 1815 | Queries.index.$in = (col, values, list) -> 1816 | return [] unless list.length 1817 | results = [] 1818 | for value in arrayize values 1819 | @_idxSearchByValue list, col, value, (obj, data) -> 1820 | keys = list.keys(obj.id) 1821 | results.push list[k] for k in keys if keys 1822 | return results 1823 | 1824 | Queries.noIndex.equal = (col, value, ids) -> 1825 | ids.filter ((id) -> 1826 | @_data[id][col] is value 1827 | ), this 1828 | 1829 | Queries.noIndex.like$ = (col, value, ids) -> 1830 | (@_colInfos[col].type is Table._STR) or err("Cannot use like$ search to a non-string column", col) 1831 | ids.filter ((id) -> 1832 | @_data[id][col].indexOf(value) is 0 1833 | ), this 1834 | 1835 | Queries.noIndex.like = (col, value, ids) -> 1836 | ids.filter ((id) -> 1837 | @_data[id][col].indexOf(value) >= 0 1838 | ), this 1839 | 1840 | Queries.noIndex.gt = (col, value, ids) -> 1841 | ids.filter ((id) -> 1842 | @_data[id][col] > value 1843 | ), this 1844 | 1845 | Queries.noIndex.ge = (col, value, ids) -> 1846 | ids.filter ((id) -> 1847 | @_data[id][col] >= value 1848 | ), this 1849 | 1850 | Queries.noIndex.lt = (col, value, ids) -> 1851 | ids.filter ((id) -> 1852 | @_data[id][col] < value 1853 | ), this 1854 | 1855 | Queries.noIndex.le = (col, value, ids) -> 1856 | ids.filter ((id) -> 1857 | @_data[id][col] <= value 1858 | ), this 1859 | 1860 | Queries.noIndex.$in = (col, values, ids) -> 1861 | ids.filter ((id) -> 1862 | arrayize(values).indexOf(@_data[id][col]) >= 0 1863 | ), this 1864 | 1865 | Queries.classes.equal = (col, val, cls) -> 1866 | (if (cls[val]) then Object.keys(cls[val]) else []) 1867 | 1868 | Queries.classes.gt = (col, val, cls) -> 1869 | ret = [] 1870 | Object.keys(cls).forEach (v) -> 1871 | ret = ret.concat(Object.keys(cls[v])) if v > val 1872 | return 1873 | 1874 | ret 1875 | 1876 | Queries.classes.ge = (col, val, cls) -> 1877 | ret = [] 1878 | Object.keys(cls).forEach (v) -> 1879 | ret = ret.concat(Object.keys(cls[v])) if v >= val 1880 | return 1881 | 1882 | ret 1883 | 1884 | Queries.classes.lt = (col, val, cls) -> 1885 | ret = [] 1886 | Object.keys(cls).forEach (v) -> 1887 | ret = ret.concat(Object.keys(cls[v])) if v < val 1888 | return 1889 | 1890 | ret 1891 | 1892 | Queries.classes.le = (col, val, cls) -> 1893 | ret = [] 1894 | Object.keys(cls).forEach (v) -> 1895 | ret = ret.concat(Object.keys(cls[v])) if v <= val 1896 | return 1897 | 1898 | ret 1899 | 1900 | Queries.classes.$in = (col, vals, cls) -> 1901 | return Queries.classes.equal.call(this, col, vals, cls) unless Array.isArray(vals) 1902 | cup vals.map((v) -> 1903 | Queries.classes.equal.call this, col, v, cls 1904 | , this) 1905 | 1906 | ###################### 1907 | # UTILITY FUNCTIONS 1908 | ###################### 1909 | 1910 | # no operation 1911 | noop = -> 1912 | 1913 | # throws error 1914 | err = (args...)-> 1915 | args.push "(undocumented error)" if args.length is 0 1916 | args.unshift "[JSRel]" 1917 | throw new Error(args.join(" ")) 1918 | 1919 | ### 1920 | shallowly copies the given object 1921 | ### 1922 | copy = (obj) -> 1923 | ret = {} 1924 | for attr of obj 1925 | ret[attr] = obj[attr] if obj.hasOwnProperty(attr) 1926 | ret 1927 | 1928 | ### 1929 | deeply copies the given value 1930 | ### 1931 | deepCopy = (val) -> 1932 | return val.map(deepCopy) if Array.isArray(val) 1933 | return val if typeof val isnt "object" or val is null or val is `undefined` 1934 | ret = {} 1935 | for attr of val 1936 | ret[attr] = deepCopy val[attr] if val.hasOwnProperty attr 1937 | return ret 1938 | 1939 | # makes elements of array unique 1940 | unique = (arr) -> 1941 | o = {} 1942 | arr.filter (i) -> if i of o then false else o[i] = true 1943 | 1944 | ### 1945 | logical sum 1946 | @params arr: > 1947 | ### 1948 | cup = (arr) -> unique Array::concat.apply([], arr) 1949 | 1950 | # quote v 1951 | quo = (v) -> "\"" + v.toString().split("\"").join("\\\"") + "\"" 1952 | 1953 | # backquote v 1954 | bq = (v) -> "`" + v + "`" 1955 | 1956 | # arrayize if not 1957 | arrayize = (v, empty) -> if Array.isArray(v) then v else if (empty and not v?) then [] else [v] 1958 | 1959 | # objectize if string 1960 | objectize = (k, v) -> 1961 | return k unless typeof k is "string" 1962 | obj = {} 1963 | obj[k] = v 1964 | return obj 1965 | 1966 | # logical conjunction of arr1 and arr2 1967 | # arr1.length should be much larger than arr2.length 1968 | conjunction = (arr1, arr2) -> 1969 | hash = {} 1970 | i = 0 1971 | l = arr2.length 1972 | while i < l 1973 | hash[arr2[i]] = true 1974 | i++ 1975 | ret = [] 1976 | j = 0 1977 | l = arr1.length 1978 | while j < l 1979 | v = arr1[j] 1980 | ret.push(v) if hash[v]? 1981 | j++ 1982 | ret 1983 | 1984 | ### 1985 | generates comparison function 1986 | 1987 | @types : data type of the column(s) 1988 | @columns : column name(s) 1989 | @data : data of the column(s) 1990 | ### 1991 | generateCompare = (types, columns, data) -> 1992 | types = arrayize types 1993 | columns = arrayize columns 1994 | 1995 | if columns.length is 1 1996 | return generateCompare[Table._NUM] if columns[0] is "id" 1997 | fn = generateCompare[types[0]] 1998 | col = columns[0] 1999 | return (id1, id2) -> fn data[id1][col], data[id2][col] 2000 | 2001 | return (id1, id2) -> 2002 | a = data[id1] 2003 | b = data[id2] 2004 | for type, k in types 2005 | col = columns[k] 2006 | result = generateCompare[type](a[col], b[col]) 2007 | return result if result 2008 | return 0 2009 | 2010 | # basic comparison functions 2011 | generateCompare[Table._BOOL] = (a, b) -> if (a is b) then 0 else if a then 1 else -1 2012 | generateCompare[Table._NUM] = SortedList.compares["number"] 2013 | generateCompare[Table._STR] = SortedList.compares["string"] 2014 | 2015 | # exporting 2016 | return JSRel 2017 | -------------------------------------------------------------------------------- /src/test/reload.coffee: -------------------------------------------------------------------------------- 1 | JSRel = require('../lib/jsrel.js') 2 | vows = require('vows') 3 | assert = require('assert') 4 | fs = require("fs") 5 | 6 | filename = __dirname + "/tmp/reload" 7 | fs.unlinkSync filename if fs.existsSync filename 8 | 9 | schema = 10 | table1: 11 | col1: 1 12 | col2: true 13 | table2: 14 | col3: 1 15 | col4: false 16 | 17 | db = JSRel.use(filename, schema: schema) 18 | 19 | db.save() 20 | 21 | vows.describe('== TESTING RELOAD ==').addBatch( 22 | reload: 23 | topic: null 24 | 25 | reload: -> 26 | JSRel._dbInfos = {} # private... 27 | reloaded_db = JSRel.use(filename, schema: schema) 28 | assert.equal(reloaded_db.tables.length, 2) 29 | 30 | loaded_is_true_when_loaded: -> 31 | JSRel._dbInfos = {} # private... 32 | reloaded_db = JSRel.use(filename, schema: schema) 33 | assert.isTrue(reloaded_db.loaded) 34 | assert.isFalse(reloaded_db.created) 35 | 36 | 37 | ).export(module) 38 | -------------------------------------------------------------------------------- /test/data/artists.js: -------------------------------------------------------------------------------- 1 | var artists = {}; 2 | 3 | artists["paris match"] = [ 4 | [3, "desert moon"], 5 | [5, "OCEANSIDE LINER"], 6 | [4, "Metro"], 7 | [4, "アルメリアホテル"], 8 | [3, "F.L.B"], 9 | [4, "眠れない悲しい夜なら"], 10 | [4, "soft parage on sunset"], 11 | [4, "KISS"], 12 | [3, "Happy-Go-Round"], 13 | [3, "eternity"], 14 | [4, "I'LL BE THERE"], 15 | [4, "虹のパズル"], 16 | [4, "CAMELLIA"], 17 | [5, "STAY WITH ME"], 18 | [5, "太陽の接吻"], 19 | [4, "VOICE"], 20 | [5, "ROCKSTAR"], 21 | [3, "JILL"], 22 | [2, "PARIS STRUT"], 23 | [2, "NIGHTFLIGHT"], 24 | [3, "ANGEL"], 25 | ]; 26 | 27 | artists.orangepekoe = [ 28 | [4, "Happy Valley"], 29 | [5, "LOVE LIFE"], 30 | [3, "Calling you"], 31 | [4, "Joyful World"], 32 | [5, "やわらかな夜"], 33 | [2, "Selene"], 34 | [2, "Dream Time"], 35 | [3, "Beautifl Thing"], 36 | [4, "Honeysuckle"], 37 | [4, "bottle"], 38 | [5, "太陽のかけら"], 39 | [4, "ホットミルク"], 40 | ]; 41 | 42 | artists.jazztronik = [ 43 | [4, "Samurai"], 44 | [4, "Brisa"], 45 | [4, "Beauty Flow"], 46 | [4, "Tigar Eyes"], 47 | [4, "Tiki Tiki"], 48 | [2, "Set Free"], 49 | [3, "VERDADES"], 50 | [4, "SEARCHING FOR LOVE"], 51 | [2, "Sunshine"], 52 | [2, "For You"], 53 | [4, "七色"], 54 | [3, "Midnight at the Oasis"], 55 | ]; 56 | 57 | artists.bird = [ 58 | [4, "ハイビスカス"], 59 | [3, "サンセットドライバー"], 60 | [3, "BEATS"], 61 | [4, "夏をリザーブ"], 62 | [2, "deep breath"], 63 | ]; 64 | 65 | artists.capsule = [ 66 | [4, "tokyo smiling"], 67 | [3, "5iVE STAR"], 68 | [4, "Twinkle Twinkle Poppp!"], 69 | [4, "アイスクリーム"], 70 | [4, "未来生活"], 71 | [2, "CrazEEE Skyhopper"], 72 | [5, "idol fancy"], 73 | [5, "プラスチックガール"], 74 | [3, "テレポーテーション"], 75 | [4, "ブラウニー"], 76 | [3, "A.I. automatic infection"], 77 | [4, "カクレンボ"], 78 | [2, "cosmic tone cooking"], 79 | [3, "life style music"], 80 | ]; 81 | 82 | artists["BE THE VOICE"] = [ 83 | [4, "Ms. Beauty"], 84 | [5, "水の惑星"], 85 | [4, "Tell Me About You"], 86 | [2, "Starman"], 87 | [4, "Urashima In Deep"], 88 | [4, "記憶は夢のすべて"], 89 | [4, "Heartbreak Hotel"], 90 | [3, "Sakura"], 91 | ]; 92 | 93 | 94 | artists["土岐麻子"] = [ 95 | [4, "乱反射ガール"], 96 | [4, "プラネタリウム"], 97 | [3, "HOO-OON"], 98 | [2, "Under Surveilance"], 99 | [3, "MY SUNNY RAINY"], 100 | [5, "鎌倉"], 101 | [4, "君に胸キュン。"], 102 | [4, "夢で逢えたら"], 103 | [3, "Gift 〜あなたはマドンナ〜"], 104 | [4, "私のお気に入り"], 105 | [3, "DOWN TOWN"], 106 | ]; 107 | 108 | 109 | artists["モダーン今夜"] = [ 110 | [4, "愛しいリズム"], 111 | [4, "おやつのじかん"], 112 | [5, "オトナ"], 113 | [5, "あのフレーズ"], 114 | [2, "もぐら"], 115 | [2, "\"ららら\""], 116 | [3, "うたかた花電車"], 117 | [4, "涙の雨"], 118 | [3, "RED"], 119 | [4, "かもめ島"], 120 | [3, "かくれんぼ"], 121 | [4, "名犬ジョディー"], 122 | [3, "ちょっと酔ってただけなのさ"], 123 | ]; 124 | 125 | 126 | 127 | if (typeof exports == "object" && exports === this) module.exports = artists; 128 | -------------------------------------------------------------------------------- /test/data/genes: -------------------------------------------------------------------------------- 1 | AQP8 2 | PRPH2 3 | ALKBH4 4 | TRIM26 5 | APOBEC3C 6 | ABO 7 | NPEPPS 8 | AURKB 9 | CRYAB 10 | MAP1LC3B 11 | ALKBH6 12 | ACACA 13 | MMP20 14 | DIRAS3 15 | A2MP1 16 | ABL1 17 | ACOT9 18 | TNFRSF10A 19 | ANAPC2 20 | C3orf35 21 | FKBP5 22 | ALDH9A1 23 | RYR2 24 | KAL1 25 | SLC25A5 26 | CYP19A1 27 | AIFM1 28 | ASIP 29 | AKAP12 30 | ASB2 31 | AKR7A2 32 | ATP1B2 33 | ADAMTS10 34 | MED12 35 | IKZF3 36 | RNF213 37 | RHOJ 38 | ANAPC1 39 | ANXA4 40 | ODAM 41 | ACVR2A 42 | FOXD3 43 | ADHFE1 44 | ADRB2 45 | RAPH1 46 | ANK2 47 | PRPF6 48 | CTSH 49 | UAP1 50 | APEX2 51 | ADAMTS7 52 | ACTR1B 53 | AKAP5 54 | ASCL2 55 | GAS6 56 | DNMT1 57 | ASMTL 58 | ATP2A1 59 | AATK 60 | KISS1R 61 | NR2F2 62 | DDX41 63 | ATP10B 64 | AP2A1 65 | FOXO3 66 | APOBEC3G 67 | TLR4 68 | PDYN 69 | NME1 70 | ANGPTL6 71 | CHI3L1 72 | PRKAG1 73 | ACSL4 74 | ASS1 75 | TACC2 76 | S100A8 77 | NR5A1 78 | TNFRSF25 79 | BIN1 80 | ANXA8 81 | BCAS1 82 | CASP10 83 | ADH1C 84 | ORM2 85 | CCL27 86 | AKAP3 87 | SEPT4 88 | ATRX 89 | SPACA3 90 | ACTL6A 91 | RFC1 92 | ALKBH8 93 | RNF111 94 | ABCD1 95 | ADAMTS4 96 | SLC25A13 97 | SERPINH1 98 | PARP2 99 | ANKS1B 100 | ATP6AP2 101 | ACVR1B 102 | NAE1 103 | AFAP1 104 | ALAD 105 | GRB2 106 | PRDX5 107 | AVP 108 | ANXA13 109 | ASH2L 110 | GSN 111 | ADCY6 112 | A1BG 113 | ATP6V1F 114 | AKR1B10 115 | SSX2IP 116 | AMIGO2 117 | CENPF 118 | ANGPTL2 119 | KDM1A 120 | ATP11C 121 | ATG4C 122 | MIA3 123 | ARHGEF4 124 | PAK1 125 | ADAM22 126 | CLDN5 127 | MAP1LC3A 128 | ALAS1 129 | AGR3 130 | ORM1 131 | SLC27A3 132 | EIF4EBP1 133 | ATP9A 134 | SMPD1 135 | TRPV6 136 | AMPH 137 | AKAP6 138 | APOC3 139 | ABI2 140 | TNFSF18 141 | ZEB1 142 | ACSL3 143 | GPR182 144 | AKAP9 145 | ARPP19 146 | RFC2 147 | PSEN2 148 | KIDINS220 149 | RIPK4 150 | AK3 151 | ASPH 152 | TTPA 153 | ASL 154 | AURKC 155 | XCL1 156 | NCOA3 157 | THBD 158 | ABCB6 159 | COPS2 160 | ADH7 161 | XIAP 162 | ANXA1 163 | PCBP3 164 | APOA2 165 | APEX1 166 | FABP4 167 | HSD17B10 168 | TG 169 | RHOA 170 | TGFB1I1 171 | SERPINF2 172 | ALAS2 173 | MTDH 174 | KCNA1 175 | SYNE1 176 | ADAMTS20 177 | TGFBR1 178 | UBA1 179 | DSC2 180 | ADPRH 181 | HSD11B2 182 | ABLIM1 183 | ANKRD1 184 | BECN1 185 | UEVLD 186 | AFM 187 | OLAH 188 | IL13 189 | ADAMTS18 190 | MYLK 191 | ITGA9 192 | ARSD 193 | LARP6 194 | CECR1 195 | HN1 196 | AKR1B1 197 | SLC45A2 198 | SAE1 199 | ACIN1 200 | SOD1 201 | CYTH2 202 | POLR2K 203 | ADSL 204 | DAB2IP 205 | PRKCA 206 | ATP8B1 207 | SLC25A4 208 | AHRR 209 | AKAP13 210 | ADORA2A 211 | APOBEC3A 212 | EFNA5 213 | HSPBAP1 214 | AIFM2 215 | AQP9 216 | FNDC1 217 | NOVA2 218 | IGFALS 219 | ATAD2 220 | RGS16 221 | H19 222 | AMBP 223 | ARRB2 224 | GTF3A 225 | CFB 226 | ANXA8L2 227 | DLC1 228 | ACE2 229 | ACVRL1 230 | CHRNB1 231 | PRMT1 232 | ARHGEF15 233 | MED25 234 | EIF2C1 235 | ATP8B3 236 | THRA 237 | ATF7IP 238 | ACE 239 | CLU 240 | FDXR 241 | ASAH1 242 | CD3EAP 243 | BCL2A1 244 | TFAP2A 245 | ATP6V0A1 246 | ADAMTS2 247 | RBM26 248 | PAX6 249 | RASSF4 250 | FLNC 251 | C1QTNF9 252 | KIAA1244 253 | FGF8 254 | ABCB4 255 | GPSM1 256 | PPP1R8 257 | KCNQ1 258 | TWF2 259 | AGA 260 | ADAMTS1 261 | SHANK2 262 | ATP2B4 263 | GABARAP 264 | ADRA1A 265 | ALDH1A3 266 | ADK 267 | BAZ1A 268 | AMMECR1 269 | PRDX3 270 | AP1B1 271 | ADAMTS16 272 | ACYP2 273 | SPATA13 274 | RIOK1 275 | NAAA 276 | SERINC3 277 | TNIP3 278 | EEF1E1 279 | PKHD1 280 | PHOX2A 281 | PRKAA1 282 | PRKAA2 283 | ALDOB 284 | RASSF1 285 | ACTR3B 286 | ADNP 287 | ATP10A 288 | RNF139 289 | MAP3K6 290 | AIPL1 291 | RAC3 292 | ATP6V1G3 293 | AKR1C2 294 | S100A10 295 | RECQL4 296 | ABCC5 297 | WIPI1 298 | TNFSF13 299 | DCAF6 300 | APLNR 301 | ASCL1 302 | ABCE1 303 | ESCO1 304 | FGF1 305 | CASC5 306 | ALKBH2 307 | ABL2 308 | SRP9 309 | ATP10D 310 | LGMN 311 | CD82 312 | DCLRE1B 313 | CD46 314 | TWF1 315 | SERPINA1 316 | PRTN3 317 | LRPAP1 318 | FLNB 319 | RUNX1T1 320 | ZNF639 321 | ACTR8 322 | NCEH1 323 | ALDH3A1 324 | PYDC1 325 | HRASLS 326 | ACSL5 327 | ATG5 328 | PRKCSH 329 | FBXW7 330 | NAA10 331 | ENOX2 332 | AKAP11 333 | ALS2 334 | OPTN 335 | ITM2B 336 | ADIPOQ 337 | AR 338 | TMEM161A 339 | ARL2 340 | ARMCX3 341 | NUSAP1 342 | LPA 343 | ADCY2 344 | MLLT11 345 | RBL2 346 | DAG1 347 | ECT2 348 | ACTR2 349 | ACAT1 350 | DMP1 351 | TGFBR2 352 | PGAP3 353 | GPN1 354 | ARL1 355 | ANP32B 356 | VCP 357 | RUNX1 358 | PROM1 359 | MCF2L2 360 | ARFRP1 361 | ANGPTL4 362 | NDUFB8 363 | ABCC3 364 | CDKN2B-AS1 365 | ANTXR1 366 | ADAM17 367 | DNMBP 368 | FGD1 369 | SERPINA3 370 | NOD2 371 | ACPP 372 | TADA2A 373 | AK5 374 | ARAF 375 | POTEM 376 | DBF4 377 | ANGPT1 378 | ADAMTSL1 379 | NAT2 380 | ENPEP 381 | SOAT1 382 | PRKCD 383 | ABCG2 384 | PYCARD 385 | ALKBH1 386 | ADH1B 387 | TBC1D4 388 | CASP8 389 | TP63 390 | LMNB1 391 | GFER 392 | ARC 393 | ACO1 394 | AHI1 395 | PDS5B 396 | AKR1A1 397 | BCOR 398 | ACACB 399 | ARR3 400 | MLLT6 401 | CASP9 402 | TFAP2E 403 | ABCA2 404 | DBI 405 | PCDH8 406 | C5orf39 407 | ALDOC 408 | KAT5 409 | GKN1 410 | RAN 411 | DIO2 412 | GPT 413 | ANAPC5 414 | TNFRSF18 415 | IGHM 416 | ATG4A 417 | WWP1 418 | ARMCX1 419 | S100A9 420 | CFI 421 | ALKBH3 422 | HUWE1 423 | ERAP1 424 | ASAP1 425 | EIF1 426 | ASNA1 427 | TNFSF12 428 | CDC27 429 | ULK1 430 | KCNA5 431 | UXT 432 | ABCA5 433 | ADAM28 434 | ADAMTS5 435 | ATP11B 436 | NCOA4 437 | ALOX12 438 | ARHGAP21 439 | COL4A5 440 | GPI 441 | SEMA4D 442 | GDNF 443 | WT2 444 | ADCY8 445 | C19orf6 446 | POMC 447 | ABCC2 448 | PRDX4 449 | SLC12A6 450 | SLC38A4 451 | PPP1R16B 452 | DLX3 453 | JUP 454 | BACE2 455 | LRRK2 456 | CSRNP1 457 | ADM 458 | NR0B1 459 | PARPBP 460 | SLPI 461 | PNPLA3 462 | ARIH1 463 | ASMT 464 | MPG 465 | TAF9 466 | APOL3 467 | ACSL1 468 | TNIP1 469 | ASH1L 470 | CDC23 471 | APAF1 472 | PDPN 473 | TARDBP 474 | CHRNG 475 | ADH6 476 | ANK3 477 | CCL18 478 | AGTR2 479 | AICDA 480 | CCL22 481 | PAGE1 482 | ARL4D 483 | ATOX1 484 | ENTPD1 485 | RHOH 486 | ADH5 487 | PMAIP1 488 | APH1B 489 | ALDH3A2 490 | ATOH1 491 | NUAK1 492 | SLC39A4 493 | ACCN2 494 | PDCD6 495 | MANF 496 | DDC 497 | S100A4 498 | RTN4 499 | SYNRG 500 | C10orf116 501 | BMPR1B 502 | HSPA4L 503 | COL16A1 504 | PNPLA2 505 | RDH11 506 | TREX1 507 | ADM2 508 | CAPG 509 | SLC5A8 510 | TDP2 511 | PSMD4 512 | RTN3 513 | AHNAK 514 | ADD3 515 | ALDOA 516 | ABCF1 517 | TNIP2 518 | OCIAD1 519 | NUDT11 520 | UBE3A 521 | EPGN 522 | BIRC2 523 | NPR2 524 | PLAU 525 | ARMC9 526 | ANXA11 527 | AKAP14 528 | FRYL 529 | ARPC1A 530 | NET1 531 | CHN1 532 | FOXO4 533 | MPP1 534 | KIF22 535 | IRAK3 536 | PRDX6 537 | MYH6 538 | ANGPT4 539 | MCF2 540 | ACTG1 541 | PPP1R13B 542 | ADARB1 543 | GLI3 544 | ABCB5 545 | CNTNAP2 546 | NAT1 547 | API5 548 | ATMIN 549 | PAPPA 550 | ANXA7 551 | RFXANK 552 | ADAMTS6 553 | ACTA1 554 | AMHR2 555 | RUNX3 556 | ANK1 557 | MSC 558 | DSG2 559 | CXCL13 560 | ATP11A 561 | DCLRE1C 562 | ASCC1 563 | CLEC2B 564 | ATP12A 565 | ANKH 566 | RCHY1 567 | AKAP1 568 | BMPR1A 569 | AIF1 570 | AFF4 571 | MAGI1 572 | ALOX15 573 | ATP5B 574 | IREB2 575 | MED15 576 | HRH4 577 | TAP2 578 | FLNA 579 | KANK1 580 | CYP1A1 581 | FOXC1 582 | SCARA3 583 | RHOQ 584 | ACY1 585 | TRAF3IP2 586 | IKBKG 587 | ACVR2B 588 | ZNF420 589 | NAA11 590 | NEUROG3 591 | SACS 592 | APP 593 | NCSTN 594 | SLC4A1 595 | MLL2 596 | RNF25 597 | HLA-B 598 | AZU1 599 | ANAPC13 600 | AP2B1 601 | UBA2 602 | MTUS1 603 | ASCC3 604 | ACTR3 605 | MLLT10 606 | AQP1 607 | TNFRSF14 608 | MRE11A 609 | AASDH 610 | KLHL2 611 | ATP8A1 612 | NOL3 613 | ROPN1L 614 | ACTA2 615 | AVPR2 616 | TWIST1 617 | WWP2 618 | ANGPTL3 619 | RUNX2 620 | AGAP2 621 | PLEKHG2 622 | AHSA1 623 | HPS5 624 | PDCD11 625 | AMD1 626 | RND1 627 | ABCA1 628 | ALOX5 629 | SQSTM1 630 | SMG1 631 | ABCF2 632 | PSEN1 633 | KLK3 634 | CD248 635 | DNAJB11 636 | ADCK2 637 | MEF2A 638 | BLNK 639 | C2orf18 640 | ACYP1 641 | SLC10A2 642 | MYCBP 643 | AGPAT9 644 | ARAF3P 645 | ADH4 646 | ARL6IP1 647 | FHL2 648 | PLIN2 649 | SEPT9 650 | CTSB 651 | NPPA 652 | LIPH 653 | TOB1 654 | TRIM29 655 | APPL1 656 | ADIPOR1 657 | THOC4 658 | IFNGR2 659 | TNNI2 660 | EIF2C2 661 | ART1 662 | FYB 663 | OBSCN 664 | HESX1 665 | TPM2 666 | ATF6 667 | OAZ1 668 | APC2 669 | CDC16 670 | FAM175A 671 | HMHA1 672 | ACSL6 673 | TFAP2C 674 | RNF14 675 | SLC9A1 676 | ALDH1B1 677 | ALDH3B2 678 | TNFSF10 679 | ACOT7 680 | HOXA7 681 | ANGPT2 682 | ATP9B 683 | AREG 684 | AQP2 685 | ATG4B 686 | CES1 687 | MTSS1L 688 | ATF5 689 | DDOST 690 | TSPAN7 691 | APLP1 692 | PRKAG3 693 | ANAPC7 694 | ANXA10 695 | TXN 696 | ATP2A2 697 | SLC38A1 698 | CYTH3 699 | ST13 700 | ADAR 701 | ALKBH7 702 | EGR2 703 | ATP6AP1 704 | GNAS 705 | TFAP4 706 | SEPT5 707 | S1PR2 708 | GATA4 709 | APOBEC1 710 | HNRNPD 711 | ADH1A 712 | RCAN1 713 | PHF11 714 | RHOU 715 | NPR1 716 | BTG3 717 | APOBEC3B 718 | MAP3K15 719 | KLK4 720 | ALDH1A1 721 | ARRB1 722 | FOXF1 723 | RRN3 724 | CDKN2B-AS 725 | HSPA4 726 | PDE8B 727 | TSPAN1 728 | SIGLEC7 729 | LRP6 730 | CDK15 731 | HTRA1 732 | MC2R 733 | AMELX 734 | CRISP3 735 | ARPC2 736 | AES 737 | ASRGL1 738 | FOXP1 739 | PARP4 740 | ASPSCR1 741 | SOCS3 742 | APLP2 743 | CNTN4 744 | EPS15 745 | APOBEC3H 746 | EPHX3 747 | KLK15 748 | INPPL1 749 | GNAT2 750 | GPA33 751 | AKR7A3 752 | ASPM 753 | ADORA3 754 | ANXA2 755 | ALPPL2 756 | AURKAIP1 757 | EPB41L3 758 | LPAR6 759 | C3 760 | SLC4A2 761 | ENPP7 762 | ADAMTS3 763 | ACTR1A 764 | AFF1 765 | APLF 766 | ADRA2B 767 | AGTR1 768 | SYNJ2BP 769 | ALPP 770 | PDZD2 771 | APLN 772 | SGCA 773 | ATP1B1 774 | EIF4G2 775 | FAM123B 776 | SRPRB 777 | JUN 778 | BIRC3 779 | AGT 780 | ALPL 781 | CFTR 782 | IL11 783 | ACTC1 784 | CD5L 785 | DCD 786 | AHSG 787 | ARFGEF1 788 | ANXA5 789 | ITCH 790 | TNFRSF4 791 | ANXA3 792 | ADORA2B 793 | TGFB3 794 | MME 795 | RNASEH2B 796 | FOXP3 797 | PPM1H 798 | SORBS2 799 | APEH 800 | MAGEH1 801 | MTTP 802 | SETX 803 | DSTN 804 | NRG1 805 | SRC 806 | AIP 807 | MCF2L 808 | ZFHX3 809 | NLRP3 810 | ATP6V0B 811 | ENPP1 812 | CCL4 813 | CD79B 814 | ZFAND6 815 | ATP5E 816 | CIAPIN1 817 | SERPINC1 818 | ITGAD 819 | ADAM18 820 | ATP8A2 821 | MLLT3 822 | PRKAB1 823 | TP53BP2 824 | IGFBP7 825 | RND3 826 | EGR1 827 | AAVS1 828 | RETN 829 | ATF3 830 | MAP3K5 831 | NOTCH2 832 | ACO2 833 | NCOA6 834 | AKT1 835 | ABCC6 836 | ZHX2 837 | CCL17 838 | AHCYL2 839 | ALKBH5 840 | PTOV1 841 | BCAM 842 | ANPEP 843 | ACLY 844 | ATP6V0A2 845 | ARL3 846 | ANG 847 | C7orf11 848 | STS 849 | AKR1E2 850 | ACHE 851 | A2M 852 | ADAM10 853 | TSPAN32 854 | NGEF 855 | VAC14 856 | PARD3 857 | ACVR1 858 | PITX2 859 | CNTN2 860 | FBLN5 861 | ANKRD2 862 | HDAC4 863 | ECT2L 864 | BIRC5 865 | ULK2 866 | ADI1 867 | PRRX1 868 | RFC4 869 | RHOD 870 | CX3CL1 871 | SOAT2 872 | C12orf48 873 | GABARAPL1 874 | ALYREF 875 | NEUROG1 876 | CDNF 877 | AKAP8 878 | ARPC1B 879 | ANLN 880 | AIFM3 881 | MAGI2 882 | TMF1 883 | PARK2 884 | CREB3L4 885 | ATP6V0A4 886 | PRKAG2 887 | ACTR5 888 | C1orf135 889 | AMLCR2 890 | MLLT4 891 | AURKA 892 | NUDT6 893 | CCNL1 894 | PRNP 895 | RHOG 896 | VWA2 897 | EDNRB 898 | DYNC2H1 899 | HEATR6 900 | APOL6 901 | SLC38A2 902 | TEAD1 903 | AKAP7 904 | ADAM12 905 | ASAP3 906 | ARAF2P 907 | PARP3 908 | MED23 909 | FASLG 910 | IGFL1 911 | MLL 912 | MECP2 913 | TAP1 914 | LRP1 915 | CHD1L 916 | AK2 917 | PKD2 918 | PLA2G16 919 | SRSF1 920 | ADSS 921 | APRT 922 | CFH 923 | ABCB11 924 | APOE 925 | ATXN3 926 | SMPDL3A 927 | DPAGT1 928 | DCAF7 929 | AIG1 930 | SART1 931 | RYBP 932 | SLURP1 933 | PROC 934 | MYBL1 935 | FOSB 936 | KCNJ2 937 | LMTK2 938 | TADA3 939 | ADC 940 | FAS 941 | STRADB 942 | ACER2 943 | LGI1 944 | PDCD6IP 945 | FGF23 946 | DPP4 947 | ADRA1B 948 | ANGPTL1 949 | TNK2 950 | ACSS2 951 | ICOS 952 | BTK 953 | RASD1 954 | SHBG 955 | ATP6V0C 956 | CDH1 957 | IGFBP1 958 | ATP6V1C1 959 | ADRM1 960 | ACCN1 961 | PGM3 962 | ABCA3 963 | GAGE12I 964 | GLS 965 | ARMCX2 966 | IGBP1 967 | RHOT1 968 | ALDH4A1 969 | SH2B2 970 | AXIN1 971 | AGR2 972 | ADIPOR2 973 | STAT3 974 | SEC31A 975 | RERE 976 | SOX2 977 | NUDT2 978 | AKAP4 979 | AIRE 980 | IFNAR1 981 | AP3D1 982 | TNFAIP3 983 | ATF7 984 | AMY2A 985 | AAAS 986 | AP1M2 987 | CD9 988 | ACVR1C 989 | EPHA2 990 | FBF1 991 | PRC1 992 | ADAMTS8 993 | ADRB1 994 | ENPP2 995 | ZNF217 996 | FPR2 997 | SRGAP1 998 | PAPSS1 999 | F8 1000 | ACTN4 1001 | CST3 1002 | ANKRD11 1003 | PRUNE2 1004 | COL2A1 1005 | EIF2C4 1006 | ARL11 1007 | PDLIM3 1008 | ATG4D 1009 | KLF12 1010 | ACADVL 1011 | IQSEC1 1012 | SRGAP2 1013 | ALDH7A1 1014 | ANXA6 1015 | ATPIF1 1016 | ADCY7 1017 | NRF1 1018 | AP1S1 1019 | SLC1A5 1020 | PARP1 1021 | CHN2 1022 | PRPS1 1023 | A4GALT 1024 | ARF4 1025 | CCDC88A 1026 | FLVCR1 1027 | FOS 1028 | TFAP2B 1029 | RHOB 1030 | APTX 1031 | TRIO 1032 | AP2M1 1033 | -------------------------------------------------------------------------------- /test/dcrud.js: -------------------------------------------------------------------------------- 1 | require("termcolor").define; 2 | var JSRel = require('../lib/jsrel.js'); 3 | var vows = require('vows'); 4 | var assert = require('assert'); 5 | var fs = require("fs"); 6 | 7 | var artists = require(__dirname + '/data/artists'); 8 | 9 | var db = JSRel.use(__dirname + "/tmp/crud", { 10 | storage: 'file', 11 | schema: { 12 | user : { 13 | name: true, 14 | mail: true, 15 | age : 0, 16 | is_activated: "on", 17 | $indexes: "name", 18 | $uniques: [["name", "mail"]] 19 | }, 20 | book : { 21 | title: true, 22 | ISBN : true, 23 | ASIN: true, 24 | price: 1, 25 | $indexes: "title", 26 | $uniques: ["ISBN", "ASIN"] 27 | }, 28 | user_book: { 29 | u : "user", 30 | b : "book" 31 | }, 32 | tag : { 33 | word: true, 34 | allow_null_column: false, 35 | type: 1, 36 | is_activated: "on", 37 | $uniques: "word", 38 | $classes: ["is_activated", "type"] 39 | }, 40 | book_tag : { 41 | b : "book", 42 | t : "tag" 43 | }, 44 | artist: { 45 | name: true 46 | }, 47 | song : { 48 | title: true, 49 | rate : 1, 50 | artist: "artist", 51 | $indexes: "title" 52 | }, 53 | 54 | song_tag: { 55 | song: "song", 56 | tag : "tag" 57 | } 58 | } 59 | }); 60 | 61 | var tagTbl = db.table("tag"); 62 | fs.readFileSync(__dirname + '/data/genes', 'utf8').trim().split("\n").forEach(function(wd, k) { 63 | tagTbl.ins({word: wd, type: (k%5) +1}); 64 | }); 65 | 66 | var artistTbl = db.table("artist"); 67 | var songTbl = db.table("song"); 68 | var songTagTbl = db.table("song_tag"); 69 | Object.keys(artists).forEach(function(name) { 70 | var artist = artistTbl.ins({name: name}); 71 | artists[name].forEach(function(song) { 72 | var song = songTbl.ins({ title: song[1], rate: song[0], artist: artist }); 73 | songTagTbl.ins({song: song, tag_id : song.id * 2 }); 74 | songTagTbl.ins({song: song, tag_id : song.id * 3 }); 75 | songTagTbl.ins({song: song, tag_id : song.id * 5 }); 76 | }); 77 | }); 78 | 79 | artistTbl.ins({name: "shinout"}); // who has no songs. (actually, I have some in MySpace!!) 80 | 81 | vows.describe('== TESTING CRUD ==').addBatch({ 82 | 83 | "db": { 84 | topic: db, 85 | 86 | // "Storage type is mock" : function(jsrel) { 87 | // assert.equal(jsrel._storage, "mock"); 88 | // } 89 | }, 90 | 91 | "trying to use undefined table": { 92 | topic : function() { 93 | try { return db.ins('xxx_table', {name: "shinout"}) } 94 | catch (e) { return e.message } 95 | }, 96 | 97 | "is invalid" : function(result) { 98 | assert.match(result, /invalid table name "xxx_table"/); 99 | } 100 | }, 101 | 102 | "search" : { 103 | topic : db.table('tag'), 104 | "undefined condition" : function(tagTbl) { 105 | try { 106 | tagTbl.find({word: {xxx: true}}); 107 | assert.fail(); 108 | } 109 | catch (e) { 110 | assert.match(e.message, /undefined condition/); 111 | } 112 | }, 113 | 114 | "undefined column" : function(tagTbl) { 115 | try { 116 | var res = tagTbl.find({xxx: 1}); 117 | assert.fail(); 118 | } 119 | catch (e) { 120 | assert.match(e.message, /unknown column/); 121 | } 122 | }, 123 | 124 | "all" : function(tagTbl) { 125 | assert.equal(tagTbl.find().length, tagTbl.count()); 126 | }, 127 | 128 | "all( find null)" : function(tagTbl) { 129 | assert.equal(tagTbl.find(null).length, tagTbl.count()); 130 | }, 131 | 132 | "the number of entries" : function(tagTbl) { 133 | assert.equal(tagTbl.count(), tagTbl._indexes.id.length); 134 | }, 135 | 136 | "get TLR4" : function(tagTbl) { 137 | var TLR4 = tagTbl.one({word: "TLR4"}); 138 | assert.equal(TLR4.word, "TLR4"); 139 | }, 140 | 141 | "get AQP%" : function(tagTbl) { 142 | var AQPs = tagTbl.find({word: {like$: "AQP"}}); 143 | assert.lengthOf(AQPs, 4); 144 | }, 145 | 146 | "get AURKA%" : function(tagTbl) { 147 | var AURKAs = tagTbl.find({word: {like$: "AURKA"}}); 148 | assert.lengthOf(AURKAs, 2); 149 | }, 150 | 151 | "get AURKA% or AQP%" : function(tagTbl) { 152 | var AURKA_OR_AQP = tagTbl.find({word: [{like$: "AURKA"}, {like$: "AQP"}]}); 153 | assert.lengthOf(AURKA_OR_AQP, 4 + 2); 154 | }, 155 | 156 | "get AURKA% and KAIP%" : function(tagTbl) { 157 | var AURKA_AND_KAIP= tagTbl.find({word: {like$: ["AURKA", "KAIP"]}}); 158 | assert.lengthOf(AURKA_AND_KAIP, 1); 159 | }, 160 | 161 | "get AURKA or AQP1" : function(tagTbl) { 162 | var AURKA_OR_AQP1 = tagTbl.find({word: {$in: ["AURKA", "AQP1"]}}); 163 | assert.lengthOf(AURKA_OR_AQP1, 2); 164 | }, 165 | 166 | "get AURKA or AQP1 using equal" : function(tagTbl) { 167 | var AURKA_OR_AQP1 = tagTbl.find({word: ["AURKA", "AQP1"]}); 168 | assert.lengthOf(AURKA_OR_AQP1, 2); 169 | }, 170 | 171 | "get AURKA% or AQP1" : function(tagTbl) { 172 | var AURKA$_OR_AQP1 = tagTbl.find({word: [ 173 | {like$: "AURKA"}, 174 | {equal: "AQP1"} 175 | ]}); 176 | assert.lengthOf(AURKA$_OR_AQP1, 3); 177 | }, 178 | 179 | "get AURKA% and AURKAIP1" : function(tagTbl) { 180 | var AURKA$_OR_AQP1 = tagTbl.find({word: { 181 | like$: "AURKA", 182 | equal: "AURKAIP1" 183 | }}); 184 | assert.lengthOf(AURKA$_OR_AQP1, 1); 185 | }, 186 | 187 | "get %BP%" : function(tagTbl) { 188 | var BPs = tagTbl.find({word: {like: "BP"}}); 189 | assert.lengthOf(BPs, 15); 190 | }, 191 | 192 | "select" : function(tagTbl) { 193 | var AURKAs = tagTbl.find({word: {like$: "AURKA"}}, {select: "word"}); 194 | assert.lengthOf(AURKAs, 2); 195 | assert.equal(AURKAs[0], "AURKA"); 196 | assert.equal(AURKAs[1], "AURKAIP1"); 197 | }, 198 | 199 | "isNull": function(tagTbl) { 200 | var nullCols = tagTbl.find({allow_null_column: null}); 201 | assert.lengthOf(nullCols, 1032); 202 | }, 203 | 204 | "groupBy": function(tagTbl) { 205 | var groupBy = tagTbl.find(null, {select: ["id", "word"], groupBy: true }); 206 | assert.lengthOf(Object.keys(groupBy), 1032); 207 | } 208 | }, 209 | 210 | "search by": { 211 | topic : function() { 212 | return db.table('tag'); 213 | }, 214 | 215 | "select id from classes" : function(tbl) { 216 | var report = {}; 217 | var results = tbl.find({type: 4}, {explain: report, select: "id", limit: 10, order : {id: "desc"}}); 218 | assert.equal(report.searches[0].searchType, "classes"); 219 | results.forEach(function(v, k) { 220 | if (results[k+1] == null) return; 221 | assert.isTrue(v > results[k+1]); 222 | }); 223 | assert.equal(report.searches[0].searchType, "classes"); 224 | assert.typeOf(results[0], "number"); 225 | }, 226 | 227 | "classes and index (merge)" : function(tbl) { 228 | var report = {}; 229 | var results = tbl.find({is_activated: true, word: {like$: "L"}}, {explain: report}); 230 | assert.equal(report.searches[0].searchType, "classes"); 231 | assert.equal(report.searches[1].searchType, "index"); 232 | assert.lengthOf(results, 12); 233 | }, 234 | 235 | "index and noIndex" : function(tbl) { 236 | var report = {}; 237 | var results = tbl.find({word: {like$: "L"}, is_activated: true}, {explain: report}); 238 | assert.equal(report.searches[0].searchType, "index"); 239 | assert.equal(report.searches[1].searchType, "noIndex"); 240 | assert.lengthOf(results, 12); 241 | }, 242 | 243 | "classes and classes (merge)" : function(tbl) { 244 | var report = {}; 245 | var results = tbl.find({type: {ge: 3, le: 4}}, {explain: report}); 246 | assert.equal(report.searches[0].searchType, "classes"); 247 | assert.equal(report.searches[1].searchType, "classes"); 248 | assert.lengthOf(results, Math.floor(1032*2/5)); 249 | }, 250 | 251 | "classes and index (union)" : function(tbl) { 252 | var report = {}; 253 | var results = tbl.find([{type: 2}, {word: {like$: "ABL1"}}], {explain: report}); 254 | assert.equal(report.searches[0].searchType, "classes"); 255 | assert.equal(report.searches[1].searchType, "index"); 256 | // note that type of ABL1 is not 2! 257 | assert.lengthOf(results, Math.floor(1032/5)+1+1); 258 | }, 259 | }, 260 | 261 | "join N:1": { 262 | topic : db.table('song'), 263 | 264 | "invalid relation column": function(tbl) { 265 | try { 266 | var song30 = tbl.one({id: 30}, {join: "xxxxxx"}); 267 | } 268 | catch (e) { 269 | assert.match(e.message, /table "xxxxxx" is not referring table "song"/); 270 | } 271 | }, 272 | 273 | "true" : function(tbl) { 274 | var song3 = tbl.one({id: 3}, {join: true}); 275 | assert.equal(song3.artist.name, "paris match"); 276 | }, 277 | 278 | "column name (one)": function(tbl) { 279 | var song30 = tbl.one({id: 30}, {join: "artist"}); 280 | assert.equal(song30.artist.name, "orangepekoe"); 281 | }, 282 | 283 | "column name (find)": function(tbl) { 284 | var song51_56 = tbl.find({id: {gt: 51, lt: 56}}, {join: "artist"}); 285 | song51_56.forEach(function(song) { 286 | assert.equal(song.artist.name, "capsule"); 287 | }); 288 | }, 289 | 290 | "with conditions": function(tbl) { 291 | var song30 = tbl.one({id: 30}, {join: {artist: {name: "orangepekoe"} } }); 292 | assert.equal(song30.artist.name, "orangepekoe"); 293 | }, 294 | 295 | "with conditions (null)": function(tbl) { 296 | var report = {}; 297 | var song30 = tbl.one({id: 30}, {join: {artist: {name: "paris match"} }, explain : report }); 298 | assert.isNull(song30); 299 | }, 300 | 301 | "with select": function(tbl) { 302 | var artists = tbl.find({id: {$in: [31,42,63,64,45]}}, {join: "artist", select: "artist" }); 303 | assert.lengthOf(artists, 5); 304 | assert.equal(artists[0].name, "orangepekoe"); 305 | assert.equal(artists[1].name, "jazztronik"); 306 | }, 307 | 308 | }, 309 | 310 | "join 1:N": { 311 | topic : db.table('artist'), 312 | "table name (one)": function(tbl) { 313 | var report = {}; 314 | var pm = tbl.one( 315 | { name: "paris match"}, 316 | { join: 317 | { song : 318 | { order : { rate: "desc" }, 319 | offset: 4, 320 | limit : 10 321 | } 322 | }, 323 | explain : report 324 | } 325 | ); 326 | assert.lengthOf(pm.song, 10); 327 | assert.equal(pm.song[0].rate, 4); 328 | }, 329 | 330 | "tablename.columnname": function(tbl) { 331 | var report = {}; 332 | var pm = tbl.one( 333 | { name: "paris match"}, 334 | { join: 335 | { "song.artist" : 336 | { order : { rate: "desc" }, 337 | offset: 10, 338 | limit : 10, 339 | as: "songs" 340 | } 341 | }, 342 | explain : report 343 | } 344 | ); 345 | // assert.equal(pm.songs[9].rate, 2); 346 | // assert.equal(pm.songs[3].title, "eternity"); 347 | }, 348 | 349 | "as songs": function(tbl) { 350 | var report = {}; 351 | var artists = tbl.find(null, {join: {song : {order: {rate: "desc"}, limit: 5, as: "songs" } }, explain: report }); 352 | artists.forEach(function(artist) { 353 | assert.lengthOf(artist.songs, 5); 354 | }) 355 | }, 356 | 357 | "inner join": function(tbl) { 358 | var report = {}; 359 | var artists = tbl.find(null, {join: {song : {order: {rate: "desc"}, as: "songs" } }, explain: report }); 360 | assert.lengthOf(artists, 8); 361 | }, 362 | 363 | "outer join": function(tbl) { 364 | var report = {}; 365 | var shinout = tbl.one({name: "shinout"}, {join: {song : {order: {rate: "desc"}, as: "songs", outer: true } }, explain: report }); 366 | assert.isNull(shinout.songs); 367 | }, 368 | 369 | "outer join (array)": function(tbl) { 370 | var report = {}; 371 | var shinout = tbl.one({name: "shinout"}, {join: {song : {order: {rate: "desc"}, as: "songs", outer: "array" } }, explain: report }); 372 | assert.lengthOf(shinout.songs, 0); 373 | }, 374 | 375 | }, 376 | 377 | "join N:M": { 378 | topic : db.table('song'), 379 | "column name (one)": function(tbl) { 380 | var report = {}; 381 | var song = tbl.one( 382 | { title: {like$: "アルメリア"} 383 | }, 384 | { explain: report, 385 | join: { tag : { via : "song_tag", as : "tags", word: {like$: "A"} } } 386 | } 387 | ); 388 | assert.lengthOf(song.tags, 2); 389 | assert.equal(song.tags[0].word, "AURKB"); 390 | }, 391 | 392 | "outer": function(tbl) { 393 | var report = {}; 394 | var song = tbl.one( 395 | { title: {like$: "アルメ"} 396 | }, 397 | { explain: report, 398 | join: { tag : { via : "song_tag", as : "tags", outer: true, word: {like$: "cccc"} } } 399 | } 400 | ); 401 | assert.isNull(song.tags); 402 | }, 403 | }, 404 | 405 | 406 | "trying to search to an empty table": { 407 | topic : db.table('book_tag'), 408 | 409 | "empty query, empty result" : function(tbl) { 410 | assert.lengthOf(tbl.find(), 0); 411 | }, 412 | 413 | "one(1) -> null" : function(tbl) { 414 | assert.isNull(tbl.one(1)); 415 | }, 416 | 417 | "simple query -> empty result" : function(tbl) { 418 | assert.lengthOf(tbl.find({t_id : {gt: 1}}), 0); 419 | } 420 | }, 421 | 422 | "insert with no value": { 423 | topic : function() { 424 | try { return db.ins('user') } 425 | catch (e) { return e.message } 426 | }, 427 | 428 | "is invalid" : function(result) { 429 | assert.match(result, /You must pass object/); 430 | }, 431 | }, 432 | 433 | "insert with empty object": { 434 | topic : function() { 435 | try { return db.ins('user', {}) } 436 | catch (e) { return e.message } 437 | }, 438 | 439 | "is invalid" : function(result) { 440 | assert.match(result, /column "[a-z0-9]+" is required/); 441 | } 442 | }, 443 | 444 | "When integers are put to string columns,": { 445 | topic : function() { 446 | return db.ins('user', {name: 123, mail: 456}); 447 | }, 448 | 449 | "converted to string" : function(obj) { 450 | assert.strictEqual(obj.name, "123"); 451 | assert.strictEqual(obj.mail, "456"); 452 | }, 453 | 454 | "default value is true when 'on' is used" : function(obj) { 455 | assert.isTrue(obj.is_activated); 456 | }, 457 | 458 | "no default value is set to 'age'" : function(obj) { 459 | assert.isNull(obj.age); 460 | }, 461 | 462 | "initial id === 1" : function(obj) { 463 | assert.strictEqual(obj.id, 1); 464 | }, 465 | 466 | "timestamp exists" : function(obj) { 467 | assert.isNumber(obj.ins_at); 468 | assert.isNumber(obj.upd_at); 469 | assert.equal(obj.ins_at, obj.upd_at); 470 | assert.ok(new Date().getTime() - obj.ins_at < 1000 ); 471 | }, 472 | 473 | }, 474 | 475 | "When invalid strings are put to number columns,": { 476 | topic : function() { 477 | try { return db.ins('book', {title: "t", price: "INVALID_PRICE", ISBN: "0226259935", ASIN: "B000J95OE4"}) } 478 | catch (e) { return e.message } 479 | }, 480 | 481 | "NaN" : function(msg) { 482 | assert.match(msg, /"INVALID_PRICE" is not a valid number/); 483 | } 484 | }, 485 | 486 | "When number strings are put to number columns,": { 487 | topic : function() { 488 | return db.ins('book', {title: "t", price: "1200", ISBN: "0226259935", ASIN: "B000J95OE4"}) 489 | }, 490 | 491 | "numberized" : function(obj) { 492 | assert.strictEqual(obj.price, 1200); 493 | }, 494 | 495 | "initial id === 1" : function(obj) { 496 | assert.strictEqual(obj.id, 1); 497 | } 498 | }, 499 | 500 | "When an invalid relation id is set,": { 501 | topic : function() { 502 | try { return db.ins('user_book', {u_id: 1, b_id: 2}) } 503 | catch (e) { return e.message } 504 | }, 505 | 506 | "an exception thrown." : function(msg) { 507 | assert.match(msg, /invalid external id/); 508 | } 509 | }, 510 | 511 | "Inserting a relation by object,": { 512 | topic : function() { 513 | return db.ins('user_book', {u: {id: 1}, b: {id: 1}}); 514 | }, 515 | 516 | "the returned value contains xxx_id" : function(obj) { 517 | assert.strictEqual(obj.u_id, 1); 518 | assert.strictEqual(obj.b_id, 1); 519 | } 520 | }, 521 | 522 | "updating": { 523 | topic : db.table('tag'), 524 | 525 | "UpdateById" : function(tagTbl) { 526 | var gene10 = tagTbl.one(10); 527 | gene10.is_activated = false; 528 | var result = tagTbl.upd(gene10); 529 | assert.isFalse(result.is_activated); 530 | var result1 = tagTbl.one({id: 10, is_activated: true}); 531 | assert.isNull(result1); 532 | }, 533 | 534 | "invalid external id" : function(tbl) { 535 | var stTable = db.table("song_tag"); 536 | var tag120 = stTable.one({tag_id: 120}); 537 | tag120.tag_id = 12345; 538 | try { 539 | stTable.upd(tag120); 540 | } 541 | catch (e) { 542 | assert.match(e.message, /invalid external id/); 543 | } 544 | }, 545 | 546 | "classes": function(tbl) { 547 | var report = {} 548 | var tag = tbl.one(82); 549 | assert.equal(tag.type, 2); 550 | tag.type = 4; 551 | tbl.upd(tag); 552 | var type2s = db.one('tag', {type: 2, id: 82}, {explain: report}); 553 | assert.equal(report.searches[0].searchType, "classes"); 554 | assert.isNull(type2s); 555 | 556 | report = {} 557 | var type4s = db.one('tag', {type: 4, id: 82}, {explain: report}); 558 | assert.equal(report.searches[0].searchType, "classes"); 559 | assert.equal(type4s.id, 82); 560 | assert.equal(type4s.type, 4); 561 | }, 562 | 563 | "indexes": function(tbl) { 564 | var report = {} 565 | var tag = tbl.one(95); 566 | word = tag.word; 567 | tag.word = "すまん、こんな値にして..."; 568 | tbl.upd(tag); 569 | var v1 = db.one('tag', {word: word}, {explain: report}); 570 | assert.equal(report.searches[0].searchType, "index"); 571 | assert.isNull(v1); 572 | 573 | report = {} 574 | var v2 = db.one('tag', {word: tag.word}, {explain: report}); 575 | assert.equal(report.searches[0].searchType, "index"); 576 | assert.equal(v2.id, 95); 577 | assert.equal(v2.word, tag.word); 578 | }, 579 | 580 | "relations1 : new values": function(tbl) { 581 | var tagJoinSongs = tbl.one(30, {join : "song_tag"}); 582 | var len = tagJoinSongs.song_tag.length; 583 | tagJoinSongs.song_tag.push({song_id: 19}); 584 | tagJoinSongs.song_tag.push({song_id: 23}); 585 | tbl.upd(tagJoinSongs); 586 | var tagJoinSongs2 = tbl.one(30, {join : "song_tag"}); 587 | assert.lengthOf(tagJoinSongs2.song_tag, len + 2); 588 | }, 589 | 590 | "relations2 : update values": function(tbl) { 591 | var tagJoinSongs = tbl.one(30, {join : "song_tag"}); 592 | var len = tagJoinSongs.song_tag.length; 593 | tagJoinSongs.song_tag.pop(); 594 | tagJoinSongs.song_tag.shift(); 595 | tagJoinSongs.song_tag[0].song_id = 55; 596 | 597 | tagJoinSongs.song_tag.push({song_id: 29}); 598 | tbl.upd(tagJoinSongs); 599 | var tagJoinSongs2 = tbl.one(30, {join : "song_tag"}); 600 | assert.lengthOf(tagJoinSongs2.song_tag, len - 1); 601 | assert.equal(tagJoinSongs2.song_tag[0].song_id, 55); 602 | }, 603 | 604 | "relations3 : append values": function(tbl) { 605 | var tagJoinSongs = tbl.one(33, {join : "song_tag"}); 606 | var len = tagJoinSongs.song_tag.length; 607 | delete tagJoinSongs.song_tag; 608 | tagJoinSongs.song_tag = [{song_id : 57}, {song_id: 77}]; 609 | tbl.upd(tagJoinSongs, { append: true }); 610 | var tagJoinSongs2 = tbl.one(33, {join : "song_tag"}); 611 | assert.lengthOf(tagJoinSongs2.song_tag, len + 2); 612 | }, 613 | 614 | "relations4 : xxx_id and xxx: xxx is prior": function(tbl) { 615 | var newSongTag = db.ins("song_tag", {song_id: 1, tag_id: 1}); 616 | newSongTag.song = db.one("song", {id: 4}); 617 | newSongTag.song_id = 6; 618 | db.upd("song_tag", newSongTag); 619 | var newSongTag2 = db.one("song_tag", newSongTag.id); 620 | assert.equal(newSongTag2.song_id, 4); 621 | } 622 | 623 | }, 624 | 625 | "deleting": { 626 | topic : db, 627 | 628 | "also related data" : function(db) { 629 | var tag1000 = db.one('tag', 20, {join: "song_tag"}); 630 | var song_tags = tag1000.song_tag; 631 | assert.lengthOf(song_tags, 2); 632 | db.del('tag', 20); 633 | 634 | song_tags.forEach(function(st) { 635 | var v = db.one("song_tag", st.id); 636 | assert.isNull(v); 637 | }); 638 | }, 639 | 640 | "classes" : function(db) { 641 | var tag = db.one('tag', 22); 642 | assert.equal(tag.type, 2); 643 | db.del('tag', 22); 644 | var report = {}; 645 | var type2s = db.one('tag', {type: 2, id: 22}, {explain: report}); 646 | assert.equal(report.searches[0].searchType, "classes"); 647 | assert.isNull(type2s); 648 | }, 649 | 650 | "index" : function(db) { 651 | var tag = db.one('tag', 23); 652 | var word = tag.word; 653 | db.del('tag', 23); 654 | var report = {}; 655 | var result = db.one('tag', {word: word}, {explain: report}); 656 | assert.equal(report.searches[0].searchType, "index"); 657 | assert.isNull(result); 658 | } 659 | }, 660 | 661 | "inserting": { 662 | topic : db, 663 | 664 | "with 1:N relations" : function() { 665 | var newUser = { name: "user1234", mail: "user1234@user1234.com" }; 666 | newUser.user_book = []; 667 | for (var k=1; k<=10; k++) { 668 | var bookData = { title: "book" + k, ISBN : "ISBN" + k, ASIN: "ASIN" + k, price: k * 1000 }; 669 | var newId = db.table("book").ins(bookData).id; 670 | newUser.user_book.push({ b_id : newId }); 671 | } 672 | var newId = db.ins("user", newUser).id; 673 | var newU = db.one("user", newId, { join : { b : { via : "user_book" } } }); 674 | assert.lengthOf(newU.b, 10); 675 | } 676 | }, 677 | 678 | 679 | 680 | }).export(module); 681 | -------------------------------------------------------------------------------- /test/hooks.js: -------------------------------------------------------------------------------- 1 | var JSRel = require('../lib/jsrel.js'); 2 | var vows = require('vows'); 3 | var assert = require('assert'); 4 | var fs = require("fs"); 5 | var Table = JSRel.Table; 6 | 7 | var artists = require(__dirname + '/data/artists'); 8 | 9 | var db = JSRel.use("hook_test", { 10 | storage: 'mock', 11 | schema: { 12 | user : { 13 | name: true, 14 | mail: true, 15 | age : 0, 16 | is_activated: "on", 17 | $indexes: "name", 18 | $uniques: [["name", "mail"]] 19 | }, 20 | book : { 21 | title: true, 22 | ISBN : true, 23 | ASIN: true, 24 | price: 1, 25 | $indexes: "title", 26 | $uniques: ["ISBN", "ASIN"] 27 | }, 28 | user_book: { 29 | u : "user", 30 | b : "book" 31 | }, 32 | tag : { 33 | word: true, 34 | type: 1, 35 | is_activated: "on", 36 | $uniques: "word", 37 | $classes: ["is_activated", "type"] 38 | }, 39 | book_tag : { 40 | b : "book", 41 | t : "tag" 42 | }, 43 | artist: { 44 | name: true 45 | }, 46 | song : { 47 | title: true, 48 | rate : 1, 49 | artist: "artist", 50 | $indexes: "title" 51 | }, 52 | 53 | song_tag: { 54 | song: "song", 55 | tag : "tag" 56 | } 57 | } 58 | }); 59 | 60 | var tagTbl = db.table("tag"); 61 | fs.readFileSync(__dirname + '/data/genes', 'utf8').trim().split("\n").forEach(function(wd, k) { 62 | tagTbl.ins({word: wd, type: (k%5) +1}); 63 | }); 64 | 65 | var artistTbl = db.table("artist"); 66 | var songTbl = db.table("song"); 67 | var songTagTbl = db.table("song_tag"); 68 | Object.keys(artists).forEach(function(name) { 69 | var artist = artistTbl.ins({name: name}); 70 | artists[name].forEach(function(song) { 71 | var song = songTbl.ins({ title: song[1], rate: song[0], artist: artist }); 72 | songTagTbl.ins({song: song, tag_id : song.id * 2 }); 73 | songTagTbl.ins({song: song, tag_id : song.id * 3 }); 74 | songTagTbl.ins({song: song, tag_id : song.id * 5 }); 75 | }); 76 | }); 77 | 78 | vows.describe('== TESTING HOOKS ==').addBatch({ 79 | "hooks:basic functions": { 80 | "on" : function() { 81 | db.on("event name:hogehoge", function() { 82 | }); 83 | 84 | assert.isArray(db._hooks["event name:hogehoge"]); 85 | assert.lengthOf(db._hooks["event name:hogehoge"], 1); 86 | }, 87 | "off" : function() { 88 | var fnToOff = function() {}; 89 | db.on("xxx", fnToOff); 90 | db.on("xxx", function(){}); 91 | db.on("xxx", function(){}); 92 | db.on("xxx", function(){}); 93 | 94 | assert.lengthOf(db._hooks["xxx"], 4); 95 | db.off("xxx", fnToOff); 96 | assert.lengthOf(db._hooks["xxx"], 3); 97 | }, 98 | 99 | "off all" : function() { 100 | assert.lengthOf(db._hooks["xxx"], 3); 101 | db.off("xxx"); 102 | assert.isNull(db._hooks["xxx"]); 103 | }, 104 | 105 | "emit": function() { 106 | var counter = 0; 107 | db.on("emit_test", function(){ counter++ }); 108 | db.on("emit_test", function(v){ counter+=v }); 109 | db.on("emit_test", function(){ counter+=10 }); 110 | db.on("emit_test", function(v,a){ counter+=a*v }); 111 | db._emit("emit_test", 3, 100); 112 | assert.equal(counter, 314); 113 | } 114 | }, 115 | 116 | "hooks:actual implements in JSRel": { 117 | "save": function() { 118 | var saved = false; 119 | db.on("save:start", function(origin) { 120 | if (!saved) 121 | assert.equal(origin, null); 122 | else 123 | assert.equal(origin, db.$export()); 124 | }); 125 | db.on("save:end", function(data) { 126 | assert.equal(data, db.$export()); 127 | saved = true; 128 | }); 129 | db.save(); 130 | db.save(); 131 | }, 132 | 133 | "ins": function() { 134 | db.on("ins", function(table, insObj) { 135 | assert.equal(table, "artist"); 136 | assert.isString(insObj.name); 137 | }); 138 | db.on("ins:user", function(insObj) { 139 | assert.fail("this never be called."); 140 | }); 141 | db.on("ins:artist", function(insObj) { 142 | assert.isString(insObj.name); 143 | }); 144 | 145 | db.ins("artist", {name: "Stevie Wonder"}); 146 | db.ins("artist", {name: "the Beatles"}); 147 | }, 148 | 149 | "upd": function() { 150 | db.on("upd", function(table, updObj, old, updKeys) { 151 | assert.equal(table, "song_tag"); 152 | assert.isObject(updObj); 153 | assert.isObject(old); 154 | assert.isArray(updKeys); 155 | }); 156 | db.on("upd:user", function() { 157 | assert.fail("this never be called."); 158 | }); 159 | db.on("upd:song_tag", function(updObj, old, updKeys) { 160 | assert.equal(updObj.song_id, new_song.id); 161 | assert.equal(old.song_id, old_song_id); 162 | assert.lengthOf(updKeys, 1); 163 | assert.equal(updKeys[0], "song_id"); 164 | }); 165 | 166 | var st = db.one("song_tag", {}, {join: {song:{title: "やわらかな夜"}}}); 167 | var old_song_id = st.song.id; 168 | var new_song = db.one("song", {title: "ハイビスカス"}); 169 | st.song = new_song; 170 | db.upd("song_tag", st); 171 | } 172 | } 173 | }).export(module); 174 | -------------------------------------------------------------------------------- /test/inout.js: -------------------------------------------------------------------------------- 1 | var JSRel = require('../lib/jsrel.js'); 2 | var vows = require('vows'); 3 | var assert = require('assert'); 4 | var fs = require("fs"); 5 | var Table = JSRel.Table; 6 | 7 | var artists = require(__dirname + '/data/artists'); 8 | 9 | var db = JSRel.use(__dirname + "/tmp/inout", { 10 | storage: 'mock', 11 | schema: { 12 | user : { 13 | name: true, 14 | mail: true, 15 | age : 0, 16 | is_activated: "on", 17 | $indexes: "name", 18 | $uniques: [["name", "mail"]] 19 | }, 20 | book : { 21 | title: true, 22 | ISBN : true, 23 | ASIN: true, 24 | price: 1, 25 | $indexes: "title", 26 | $uniques: ["ISBN", "ASIN"] 27 | }, 28 | user_book: { 29 | u : "user", 30 | b : "book" 31 | }, 32 | tag : { 33 | word: true, 34 | type: 1, 35 | is_activated: "on", 36 | $uniques: "word", 37 | $classes: ["is_activated", "type"] 38 | }, 39 | book_tag : { 40 | b : "book", 41 | t : "tag" 42 | }, 43 | artist: { 44 | name: true 45 | }, 46 | song : { 47 | title: true, 48 | rate : 1, 49 | artist: "artist", 50 | $indexes: "title" 51 | }, 52 | 53 | song_tag: { 54 | song: "song", 55 | tag : "tag" 56 | } 57 | } 58 | }); 59 | 60 | var emptyDB = JSRel.use(__dirname + "/tmp/empty", { schema: { 61 | tbl1: { col1: 0, col2 : true, col3: true, $indexes: ["col1", "col2"], $uniques: ["col1", ["col1", "col3"]], $classes: "col1"}, 62 | tbl2: { ex : "tbl1", col1: true, col2 : true, col3: 1, $indexes: ["col1", "col2"], $uniques: ["col1", ["col1", "col3"]], $classes: ["ex", "col3"]} 63 | }}); 64 | 65 | var tagTbl = db.table("tag"); 66 | fs.readFileSync(__dirname + '/data/genes', 'utf8').trim().split("\n").forEach(function(wd, k) { 67 | tagTbl.ins({word: wd, type: (k%5) +1}); 68 | }); 69 | 70 | var artistTbl = db.table("artist"); 71 | var songTbl = db.table("song"); 72 | var songTagTbl = db.table("song_tag"); 73 | Object.keys(artists).forEach(function(name) { 74 | var artist = artistTbl.ins({name: name}); 75 | artists[name].forEach(function(song) { 76 | var song = songTbl.ins({ title: song[1], rate: song[0], artist: artist }); 77 | songTagTbl.ins({song: song, tag_id : song.id * 2 }); 78 | songTagTbl.ins({song: song, tag_id : song.id * 3 }); 79 | songTagTbl.ins({song: song, tag_id : song.id * 5 }); 80 | }); 81 | }); 82 | 83 | 84 | var st = JSON.stringify; 85 | 86 | vows.describe('== TESTING IN/OUT ==').addBatch({ 87 | "save": { 88 | topic: db, 89 | 90 | "origin is null for the first time" : function(songTbl) { 91 | assert.isNull(db.origin()); 92 | }, 93 | 94 | "origin is equal to compressed dump" : function(songTbl) { 95 | db.save(); 96 | var origin = db.origin(); 97 | assert.isNotNull(origin); 98 | assert.equal(typeof origin, "string"); 99 | assert.equal(origin, db.$export()); 100 | }, 101 | 102 | "origin is not changed by crud" : function(songTbl) { 103 | var lastId = db.count("song"); 104 | var origin1 = db.origin(); 105 | assert.isNotNull(db.one("song", {id:lastId})); 106 | db.del("song", {id: lastId}); 107 | assert.isNull(db.one("song", {id:lastId})); 108 | var origin2 = db.origin(); 109 | assert.equal(origin1, origin2); 110 | db.save(); 111 | var origin3 = db.origin(); 112 | assert.notEqual(origin2, origin3); 113 | } 114 | }, 115 | 116 | "compression": { 117 | topic: db.table("song"), 118 | 119 | "data" : function(songTbl) { 120 | var compressed = Table._compressData(songTbl._colInfos, songTbl._data, songTbl._indexes, songTbl._idxKeys); 121 | var decompressed = Table._decompressData(compressed) 122 | var recompressed = Table._compressData(decompressed[0], decompressed[1], decompressed[2], decompressed[3]) 123 | var redecomp = Table._decompressData(recompressed); 124 | assert.deepEqual(compressed, recompressed); 125 | 126 | // console.log(st(compressed)); 127 | // console.log(st(decompressed)); 128 | assert.deepEqual(songTbl._colInfos, decompressed[0]) 129 | assert.deepEqual(songTbl._data, decompressed[1]) 130 | assert.equal(st(songTbl._indexes), st(decompressed[2])) 131 | assert.equal(st(redecomp), st(decompressed)) 132 | }, 133 | 134 | "classes": function() { 135 | var tbl = db.table("tag"); 136 | var classes = tbl._classes; 137 | var cClasses = Table._compressClasses(classes); 138 | var deClasses = Table._decompressClasses(cClasses); 139 | var recClasses = Table._compressClasses(deClasses); 140 | assert.deepEqual(classes, deClasses) 141 | assert.equal(st(classes), st(deClasses)); 142 | assert.deepEqual(cClasses, recClasses) 143 | assert.equal(st(cClasses), st(recClasses)); 144 | }, 145 | 146 | "classes (empty)": function() { 147 | var tbl = emptyDB.table("tbl2"); 148 | var classes = tbl._classes; 149 | var cClasses = Table._compressClasses(classes); 150 | var deClasses = Table._decompressClasses(cClasses); 151 | var recClasses = Table._compressClasses(deClasses); 152 | assert.deepEqual(classes, deClasses) 153 | assert.equal(st(classes), st(deClasses)); 154 | assert.deepEqual(cClasses, recClasses) 155 | assert.equal(st(cClasses), st(recClasses)); 156 | }, 157 | 158 | "rels": function() { 159 | var tbl = db.table("song"); 160 | var rels = tbl._rels; 161 | var referreds = tbl._referreds;; 162 | var cRelRefs = Table._compressRels(rels, referreds); 163 | var relRefs = Table._decompressRels(cRelRefs); 164 | assert.deepEqual([rels, referreds], relRefs) 165 | }, 166 | 167 | "all": function() { 168 | var tbl = db.table("song"); 169 | var c = tbl._compress(); 170 | tbl._parseCompressed(c); 171 | assert.equal(st(tbl._compress()), st(c)) 172 | } 173 | }, 174 | 175 | "reset db": { 176 | topic: JSRel.use("dbToReset", {schema: {hoge:{fuga:true}}}), 177 | 178 | "if no reset, loaded" : function(db) { 179 | var newDB = JSRel.use(db.id, {schema: {xxxxx: {name:true}}}); 180 | var tbl = newDB.table("xxxxx") 181 | assert.isUndefined(tbl); 182 | }, 183 | 184 | "if reset, reset" : function(db) { 185 | var newDB = JSRel.use("dbToReset", {unko: "fasd", reset: true, schema: {xxxxx: {name:true}}}); 186 | var tbl = newDB.table("xxxxx"); 187 | assert.isObject(tbl); 188 | }, 189 | 190 | }, 191 | 192 | "export/import": { 193 | topic: db, 194 | 195 | "import fails when uniqId is null" : function(v) { 196 | try { var newDB = JSRel.$import() } 197 | catch (e) { 198 | assert.match(e.message, /uniqId is required and must be non-zero value/); 199 | } 200 | }, 201 | 202 | "import fails when uniqId is duplicated" : function(v) { 203 | try { 204 | var newDB = JSRel.$import(__dirname + "/tmp/inout", db.$export()); 205 | } 206 | catch (e) { 207 | assert.match(e.message, /already exists/); 208 | } 209 | }, 210 | 211 | "import succees when uniqId is duplicated but forced" : function(v) { 212 | var newDB = JSRel.$import(__dirname + "/tmp/inout", db.$export(), {force: true}); 213 | }, 214 | 215 | "import implementation" : function(v) { 216 | var newDB = JSRel.$import(__dirname + "/tmp/inout", db.$export(), {force: true}); 217 | assert.equal(newDB.tables.length, db.tables.length); 218 | }, 219 | 220 | "cloning" : function(v) { 221 | var comp = db.$export(); 222 | var newDB = JSRel.$import("anotherId", comp); 223 | assert.equal(comp, newDB.$export()); 224 | }, 225 | 226 | "alias" : function(v) { 227 | var comp = db.$export(); 228 | var newDB = JSRel.import("importAlias", comp); 229 | assert.equal(comp, newDB.$export()); 230 | }, 231 | 232 | "cloning with invalid storage name" : function(v) { 233 | var comp = emptyDB.$export(); 234 | try { 235 | var newDB = JSRel.$import("invalidStorageName", comp, {storage: "xxx"}); 236 | } 237 | catch(e){ 238 | assert.match(e.message, /options\.storage/); 239 | } 240 | }, 241 | 242 | "cloning with options" : function(v) { 243 | var comp = emptyDB.$export(); 244 | var newDB = JSRel.$import("cloneWithOption", comp, {storage: "mock", autosave: true, name: "cloned"}); 245 | assert.equal(emptyDB._storage, "file") 246 | assert.equal(emptyDB.name, emptyDB.id) 247 | assert.isFalse(emptyDB._autosave) 248 | assert.equal(newDB._storage, "mock") 249 | assert.equal(newDB.name, "cloned") 250 | assert.isTrue(newDB._autosave) 251 | }, 252 | 253 | "cloning with empty DB (raw)" : function(v) { 254 | var dump = emptyDB.$export(true); 255 | var newDB = JSRel.$import("AnotherEmptyRaw", dump); 256 | var redump = newDB.$export(true); 257 | assert.equal(dump, redump) 258 | }, 259 | 260 | "compression rate" : function(v) { 261 | var comp = db.$export(); 262 | var nocomp = db.$export(true); 263 | assert.isTrue(comp.length * 2 < nocomp.length) 264 | }, 265 | 266 | "get canonical schema" : function(v) { 267 | var testSchema = { 268 | "table1": { 269 | defaultStr: {type: "str", required: false, _default: "original!"}, 270 | defaultOnBool: "on", 271 | name: "str", 272 | mail: "str", 273 | activated: "bool", 274 | uniqId: 1, 275 | $uniques: ["uniqId", ["name", "mail", "activated"]] 276 | }, 277 | 278 | "table2" : { 279 | col1 : {type: "boolean"}, 280 | col2 : {type: "string"}, 281 | col3 : {type: "number"}, 282 | col4 : {type: "number"}, 283 | col5 : {type: "string"}, 284 | col6 : {type: "number"}, 285 | col7 : {type: "number"}, 286 | $indexes: ["col4", ["col2", "col1", "col7"]], 287 | $uniques: ["col1", ["col2", "col3", "col5"]], 288 | $classes: ["col6", ["col3", "col4", "col7"]] 289 | } 290 | }; 291 | var db = JSRel.use("schemaTest", {schema: testSchema}); 292 | var createdSchema = db.schema; 293 | var db2 = JSRel.use("Re-Created", {schema: createdSchema}); 294 | assert.deepEqual(createdSchema, db2.schema); 295 | } 296 | }, 297 | 298 | //"free": { 299 | // topic: db, 300 | // "the db" : function(db) { 301 | // var dump = db.$export(); 302 | // JSRel.$import("newName", dump); 303 | // assert.notEqual(JSRel.uniqIds.indexOf("newName"), -1); 304 | // JSRel.free("newName"); 305 | // assert.equal(JSRel.uniqIds.indexOf("newName"), -1); 306 | // } 307 | //}, 308 | 309 | "SQL": { 310 | topic: db, 311 | 312 | "rails" : function(db) { 313 | var datetime = function(v) { 314 | function n2s(n){ return ("000"+n).match(/..$/) } 315 | var t = new Date(v); 316 | return t.getFullYear()+"-"+n2s(t.getMonth()+1)+"-"+n2s(t.getDate())+" " 317 | +n2s(t.getHours())+":"+n2s(t.getMinutes())+":"+n2s(t.getSeconds()); 318 | }; 319 | var sql = db.toSQL({ 320 | columns: {upd_at: "updated_at", ins_at: "created_at"}, 321 | values : {upd_at: datetime, ins_at: datetime}, 322 | }); 323 | 324 | var railsSQL = db.toSQL({rails: true}); 325 | assert.equal(sql, railsSQL) 326 | }, 327 | 328 | "db_custom_name" : function(db) { 329 | var sql = db.toSQL({ db: "DB_NAME" }); 330 | assert.equal(sql.indexOf('CREATE DATABASE `DB_NAME`;'), 0); 331 | }, 332 | 333 | "db_prepared_name" : function(db) { 334 | var sql = db.toSQL({ db: true }); 335 | assert.equal(sql.indexOf('CREATE DATABASE `'+ db.id + '`;'), 0); 336 | } 337 | } 338 | }).export(module); 339 | -------------------------------------------------------------------------------- /test/reload.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var JSRel, assert, db, filename, fs, schema, vows; 3 | 4 | JSRel = require('../lib/jsrel.js'); 5 | 6 | vows = require('vows'); 7 | 8 | assert = require('assert'); 9 | 10 | fs = require("fs"); 11 | 12 | filename = __dirname + "/tmp/reload"; 13 | 14 | if (fs.existsSync(filename)) { 15 | fs.unlinkSync(filename); 16 | } 17 | 18 | schema = { 19 | table1: { 20 | col1: 1, 21 | col2: true 22 | }, 23 | table2: { 24 | col3: 1, 25 | col4: false 26 | } 27 | }; 28 | 29 | db = JSRel.use(filename, { 30 | schema: schema 31 | }); 32 | 33 | db.save(); 34 | 35 | vows.describe('== TESTING RELOAD ==').addBatch({ 36 | reload: { 37 | topic: null, 38 | reload: function() { 39 | var reloaded_db; 40 | JSRel._dbInfos = {}; 41 | reloaded_db = JSRel.use(filename, { 42 | schema: schema 43 | }); 44 | return assert.equal(reloaded_db.tables.length, 2); 45 | }, 46 | loaded_is_true_when_loaded: function() { 47 | var reloaded_db; 48 | JSRel._dbInfos = {}; 49 | reloaded_db = JSRel.use(filename, { 50 | schema: schema 51 | }); 52 | assert.isTrue(reloaded_db.loaded); 53 | return assert.isFalse(reloaded_db.created); 54 | } 55 | } 56 | })["export"](module); 57 | 58 | }).call(this); 59 | -------------------------------------------------------------------------------- /test/schema.js: -------------------------------------------------------------------------------- 1 | var JSRel = require('../lib/jsrel.js'); 2 | var vows = require('vows'); 3 | var assert = require('assert'); 4 | 5 | vows.describe('== TESTING SCHEMA ==').addBatch({ 6 | "JSRel.use() with no schema in creation": { 7 | topic: function() { 8 | try { return JSRel.use("tmp/schema01", {user: { name: true } }) } 9 | catch (e) { return e.message } 10 | }, 11 | " is not allowed" : function(topic) { 12 | assert.match(topic, /options\.schema is required/); 13 | } 14 | }, 15 | 16 | "JSRel.use() with no schema in loading": { 17 | topic: null, 18 | 19 | "succeeds": function() { 20 | var id = "tmp/use_twice" 21 | var db = JSRel.create(id, {schema: {user: { xxx: 1, name: true }}}); 22 | var db2 = JSRel.use(id); 23 | assert.equal(db, db2); 24 | } 25 | }, 26 | 27 | "JSRel.create() with already existing uniqId": { 28 | topic: function() { 29 | var id = "tmp/xxx" 30 | var db = JSRel.create(id, {schema: {user: { xxx: 1, name: true }}}); 31 | try { 32 | var db2 = JSRel.create(id, {schema: {user: { xxx: 1, name: true }}}); 33 | } 34 | catch (e) { return e.message } 35 | }, 36 | " is not allowed" : function(topic) { 37 | assert.match(topic, /already exists/); 38 | } 39 | }, 40 | 41 | 42 | "JSRel.createIfNotExists() with already existing uniqId": { 43 | topic: null, 44 | 45 | "succeeds": function() { 46 | var id = "tmp/yyy" 47 | var db = JSRel.create(id, {schema: {user: { xxx: 1, name: true }}}); 48 | var db2 = JSRel.createIfNotExists(id, {schema: {user: { xxx: 1, name: true }}}); 49 | assert.equal(db, db2); 50 | } 51 | }, 52 | 53 | "A schema that has 'id' as a column name": { 54 | topic: function() { 55 | try { return JSRel.use("tmp/schema02", { schema: {user: { id: 1, name: true } }}) } 56 | catch (e) { return e.message } 57 | }, 58 | " is not allowed" : function(topic) { 59 | assert.match(topic, /id is not allowed/); 60 | } 61 | }, 62 | 63 | "A schema that has 'upd_at' as a column name": { 64 | topic: function() { 65 | try { return JSRel.use("tmp/schema03", { schema: {user: { upd_at: 1, name: true } }}) } 66 | catch (e) { return e.message } 67 | }, 68 | " is not allowed" : function(topic) { 69 | assert.match(topic, /upd_at is not allowed/); 70 | } 71 | }, 72 | 73 | "A schema that has 'bool' as a column name": { 74 | topic: function() { 75 | try { return JSRel.use("tmp/schema04", { schema: {user: { bool: false, name: true } }}) } 76 | catch (e) { return e.message } 77 | }, 78 | " is not allowed" : function(topic) { 79 | assert.match(topic, /bool is not allowed/); 80 | } 81 | }, 82 | 83 | "A schema that has 'join' as a column name": { 84 | topic: function() { 85 | try { return JSRel.use("tmp/schema100", { schema: {user: { join: false, name: true } }}) } 86 | catch (e) { return e.message } 87 | }, 88 | " is not allowed" : function(topic) { 89 | assert.match(topic, /join is not allowed/); 90 | } 91 | }, 92 | 93 | "A schema that contains ',' in a column name": { 94 | topic: function() { 95 | try { return JSRel.use("tmp/schema101", { schema: {user: { "A,B": false, name: true } }}) } 96 | catch (e) { return e.message } 97 | }, 98 | " is not allowed" : function(topic) { 99 | assert.match(topic, /cannot be included in a column name/); 100 | } 101 | }, 102 | 103 | "A schema that contains '.' in a column name": { 104 | topic: function() { 105 | try { return JSRel.use("tmp/schema102", { schema: {user: { "A.B": false, name: true } }}) } 106 | catch (e) { return e.message } 107 | }, 108 | " is not allowed" : function(topic) { 109 | assert.match(topic, /cannot be included in a column name/); 110 | } 111 | }, 112 | 113 | "A schema with no tables": { 114 | topic: function() { 115 | try { return JSRel.use("tmp/schema05", { schema: {}}) } 116 | catch (e) { return e.message } 117 | }, 118 | " is not allowed" : function(topic) { 119 | assert.match(topic, /schema must contain at least one table/); 120 | } 121 | }, 122 | 123 | "A table with no columns": { 124 | topic: function() { 125 | try { return JSRel.use("tmp/schema06", { schema: {user: {}}}) } 126 | catch (e) { return e.message } 127 | }, 128 | " is not allowed" : function(topic) { 129 | assert.match(topic, /table "user" must contain at least one column/); 130 | } 131 | }, 132 | 133 | "A schema that has unregistered indexes": { 134 | topic: function() { 135 | try { return JSRel.use("tmp/schema07", { schema: { 136 | user: { 137 | name : true, 138 | $indexes: "xxxx" 139 | } 140 | }})} 141 | catch (e) { return e.message } 142 | }, 143 | " is not allowed" : function(topic) { 144 | assert.match(topic, /"xxxx" is unregistered column. in "user"/); 145 | } 146 | }, 147 | 148 | "A schema that has unregistered classes": { 149 | topic: function() { 150 | try { return JSRel.use("tmp/schema08", { schema: { 151 | user: { 152 | name : true, 153 | $classes: "xxxx" 154 | } 155 | }})} 156 | catch (e) { return e.message } 157 | }, 158 | " is not allowed" : function(topic) { 159 | assert.match(topic, /"xxxx" is unregistered column. in "user"/); 160 | } 161 | }, 162 | 163 | "A schema that has invalid index": { 164 | topic: function() { 165 | try { return JSRel.use("tmp/schema09", { schema: { 166 | user: { 167 | name : true, 168 | $indexes : {name: true} 169 | } 170 | }})} 171 | catch (e) { return e.message } 172 | }, 173 | " is not allowed" : function(topic) { 174 | assert.match(topic, /is unregistered column. in "user"/); 175 | } 176 | }, 177 | 178 | "setting classes to string columns": { 179 | topic: function() { 180 | try { return JSRel.use("tmp/schema10", { schema: { 181 | user: { 182 | name : true, 183 | $classes : "name" 184 | } 185 | }})} 186 | catch (e) { return e.message } 187 | }, 188 | " is not allowed" : function(topic) { 189 | assert.match(topic, /Cannot set class index to string columns "name"/); 190 | } 191 | }, 192 | 193 | "setting xxx and xxx_id": { 194 | topic: function() { 195 | try { return JSRel.use("tmp/schema11", { schema: { 196 | user: { a : "a", a_id : 1 }, 197 | rel : { a : true } 198 | }})} 199 | catch (e) { return e.message } 200 | }, 201 | " is not allowed" : function(topic) { 202 | assert.match(topic, /"a_id" is already registered/); 203 | } 204 | }, 205 | 206 | "A schema": { 207 | topic: function() { 208 | return JSRel.use("tmp/tiny", { schema: { 209 | user : { 210 | name: true, 211 | mail: true, 212 | age : 0, 213 | is_activated: "on", 214 | $indexes: "name", 215 | $uniques: [["name", "mail"]], 216 | $classes: "is_activated" 217 | }, 218 | book : { 219 | title: true, 220 | ISBN : true, 221 | code : 1, 222 | $indexes: "title", 223 | $uniques: ["ISBN", "code"] 224 | }, 225 | user_book: { 226 | u : "user", 227 | b : "book" 228 | } 229 | }}) 230 | }, 231 | 232 | " generates _tblInfos" : function(jsrel) { 233 | assert.ok(jsrel._tblInfos); 234 | }, 235 | 236 | " generates two tables" : function(jsrel) { 237 | assert.equal(jsrel.tables.length, 3); 238 | }, 239 | 240 | " has table 'user'" : function(jsrel) { 241 | assert.instanceOf(jsrel.table('user'), JSRel.Table); 242 | }, 243 | 244 | " has table 'user_book'" : function(jsrel) { 245 | assert.instanceOf(jsrel.table('user_book'), JSRel.Table); 246 | }, 247 | 248 | " And book has six columns" : function(jsrel) { 249 | assert.equal(jsrel.table('book').columns.length, 6); 250 | }, 251 | 252 | "typeof column 'ISBN' is string" : function(jsrel) { 253 | assert.equal(jsrel.table('book')._colInfos.ISBN.type, JSRel.Table._STR); 254 | }, 255 | 256 | "column 'ISBN' is required" : function(jsrel) { 257 | assert.equal(jsrel.table('book')._colInfos.ISBN.required, true); 258 | }, 259 | 260 | "typeof column 'age' is number" : function(jsrel) { 261 | assert.equal(jsrel.table('user')._colInfos.age.type, JSRel.Table._NUM); 262 | }, 263 | 264 | "column 'age' is not required" : function(jsrel) { 265 | assert.equal(jsrel.table('user')._colInfos.age.required, false); 266 | }, 267 | 268 | "typeof 'is_activated' is boolean" : function(jsrel) { 269 | assert.equal(jsrel.table('user')._colInfos.is_activated.type, JSRel.Table._BOOL); 270 | }, 271 | 272 | "typeof 'ins_at' is number" : function(jsrel) { 273 | assert.equal(jsrel.table('user')._colInfos.ins_at.type, JSRel.Table._NUM); 274 | }, 275 | 276 | "typeof 'id' is number" : function(jsrel) { 277 | assert.equal(jsrel.table('user_book')._colInfos.id.type, JSRel.Table._NUM); 278 | }, 279 | 280 | "'id' is a unique column" : function(jsrel) { 281 | assert.isTrue(jsrel.table('user_book')._indexes.id._unique); 282 | }, 283 | 284 | "'ISBN' is a unique column" : function(jsrel) { 285 | assert.isTrue(jsrel.table('book')._indexes.ISBN._unique); 286 | }, 287 | 288 | "'name' is not a unique index" : function(jsrel) { 289 | assert.isFalse(jsrel.table('user')._indexes.name._unique); 290 | }, 291 | 292 | "'is_activated' is a class index" : function(jsrel) { 293 | assert.equal(jsrel.table('user')._classes.is_activated.cols[0], "is_activated"); 294 | }, 295 | 296 | "'u' is referring external table 'user'" : function(jsrel) { 297 | assert.equal(jsrel.table('user_book')._rels.u, 'user'); 298 | }, 299 | 300 | "'b_id' is not a unique index" : function(jsrel) { 301 | assert.isFalse(jsrel.table('user_book')._indexes.b_id._unique); 302 | }, 303 | 304 | "'book' is referred by 'user_book.b'" : function(jsrel) { 305 | assert.isTrue(jsrel.table('book')._referreds.user_book.hasOwnProperty("b")); 306 | }, 307 | 308 | "'book' is referred by 'user_book.b, and required'" : function(jsrel) { 309 | assert.isTrue(jsrel.table('book')._referreds.user_book.b); 310 | }, 311 | 312 | "data is empty" : function(jsrel) { 313 | assert.lengthOf(Object.keys(jsrel.table('book')._data), 0); 314 | }, 315 | 316 | "'name,mail' is a unique complex index" : function(jsrel) { 317 | assert.isTrue(jsrel.table('user')._indexes["name,mail"]._unique); 318 | }, 319 | 320 | "'name' has two indexes" : function(jsrel) { 321 | assert.lengthOf(Object.keys(jsrel.table('user')._idxKeys.name), 2); 322 | } 323 | 324 | }, 325 | "deletion of table": { 326 | topic: function() { 327 | var schema = { 328 | user : { 329 | name: true, 330 | mail: true, 331 | age : 0, 332 | is_activated: "on", 333 | $indexes: "name", 334 | $uniques: [["name", "mail"]], 335 | $classes: "is_activated" 336 | }, 337 | book : { 338 | title: true, 339 | ISBN : true, 340 | code : 1, 341 | $indexes: "title", 342 | $uniques: ["ISBN", "code"] 343 | }, 344 | user_book: { 345 | u : "user", 346 | b : "book" 347 | }, 348 | 349 | user_info: { 350 | info: true, 351 | uuuu: {type: "user", required: false} 352 | } 353 | }; 354 | return schema; 355 | }, 356 | 357 | "dropping table which is referred from other tables(should fail)" : function(schema) { 358 | var db = JSRel.use("tst1", { schema: schema }); 359 | try { 360 | db.drop("book"); 361 | assert.fail(); 362 | } 363 | catch (e) { 364 | assert.match(e.message, /"user_book"/); 365 | } 366 | }, 367 | 368 | "dropping table with referring table (should succeed)" : function(schema) { 369 | var db = JSRel.use("tst2", { schema: schema }); 370 | var initialLnegth = db.tables.length; 371 | db.drop("book", "user_book"); 372 | assert.lengthOf(db.tables, initialLnegth - 2); 373 | assert.include(db.tables, "user"); 374 | assert.include(db.tables, "user_info"); 375 | }, 376 | 377 | "dropping table which is referred from other tables but not required (should succeed)" : function(schema) { 378 | var db = JSRel.use("tst3", { schema: schema }); 379 | var shinout = db.ins("user", {name: "shinout", mail: "shinout@shinout.net"}); 380 | var info1 = db.ins("user_info", {info: "medical doctor", uuuu: shinout }); 381 | var info2 = db.ins("user_info", {info: "wanna be a scientist", uuuu: shinout }); 382 | var info3 = db.ins("user_info", {info: "plays Alto sax", uuuu: shinout }); 383 | var info4 = db.ins("user_info", {info: "plays the keyboard with transposing", uuuu: shinout }); 384 | var info5 = db.ins("user_info", {info: "begins playing the electric bass", uuuu: shinout }); 385 | 386 | assert.isNotNull(db.one("user_info", info1.uuuu_id)); 387 | assert.isNotNull(db.one("user_info", info2.uuuu_id)); 388 | assert.isNotNull(db.one("user_info", info3.uuuu_id)); 389 | assert.isNotNull(db.one("user_info", info4.uuuu_id)); 390 | assert.isNotNull(db.one("user_info", info5.uuuu_id)); 391 | 392 | var initialLnegth = db.tables.length; 393 | db.drop("user", "user_book"); 394 | assert.lengthOf(db.tables, initialLnegth - 2); 395 | assert.include(db.tables, "book"); 396 | assert.include(db.tables, "user_info"); 397 | assert.isNull(db.one("user_info", info1.id).uuuu_id); 398 | assert.isNull(db.one("user_info", info2.id).uuuu_id); 399 | assert.isNull(db.one("user_info", info3.id).uuuu_id); 400 | assert.isNull(db.one("user_info", info4.id).uuuu_id); 401 | assert.isNull(db.one("user_info", info5.id).uuuu_id); 402 | } 403 | } 404 | }).export(module); 405 | -------------------------------------------------------------------------------- /test/statics.js: -------------------------------------------------------------------------------- 1 | var JSRel = require('../lib/jsrel.js'); 2 | var vows = require('vows'); 3 | var assert = require('assert'); 4 | var schema = { 5 | user: {name : true}, 6 | book: {title: true, price: 1}, 7 | user_book: {u: "user", b: "book"}, 8 | foo : { bar: 1 } 9 | }; 10 | 11 | var db = JSRel.use("tmp/sample", { schema: schema }); 12 | 13 | vows.describe('== TESTING STATIC VALUES ==').addBatch({ 14 | "JSRel": { 15 | topic: JSRel, 16 | 17 | "is running on Node.js" : function(topic) { 18 | assert.isTrue(JSRel.isNode); 19 | }, 20 | 21 | "is not running on Browser" : function(topic) { 22 | assert.isFalse(JSRel.isBrowser); 23 | }, 24 | 25 | "has uniqIds (Array)" : function(topic) { 26 | assert.isArray(JSRel.uniqIds); 27 | }, 28 | 29 | "has storages including 'file'" : function(topic) { 30 | assert.include(JSRel.storages, 'file'); 31 | } 32 | }, 33 | 34 | "jsrel": { 35 | topic: db, 36 | 37 | "has id" : function(db) { 38 | assert.equal(db.id, 'tmp/sample'); 39 | }, 40 | 41 | "has default name" : function(db) { 42 | assert.equal(db.name, 'tmp/sample'); 43 | }, 44 | 45 | "has tables" : function(db) { 46 | assert.isArray(db.tables); 47 | }, 48 | 49 | "the number of tables" : function(db) { 50 | assert.equal(db.tables.length, Object.keys(schema).length); 51 | }, 52 | 53 | }, 54 | 55 | "JSRel.create": { 56 | topic: schema, 57 | 58 | "fails if already exists" : function(schema) { 59 | var db1 = JSRel.create("first", { schema: schema}); 60 | try { 61 | var db2 = JSRel.create("first", { schema: schema}); 62 | } 63 | catch (e) { 64 | assert.match(e.message, /uniqId "first" already exists/); 65 | } 66 | } 67 | }, 68 | 69 | "jsrel.name": { 70 | topic: JSRel.use("xxx", { schema: schema, name: "NAME" }), 71 | 72 | "can be set" : function(db) { 73 | assert.equal(db.name, 'NAME'); 74 | } 75 | }, 76 | 77 | "table": { 78 | topic: db.table('user'), 79 | "has name" : function(tbl) { 80 | assert.equal(tbl.name, "user"); 81 | }, 82 | 83 | "has columns" : function(tbl) { 84 | assert.isArray(tbl.columns); 85 | }, 86 | } 87 | 88 | }).export(module); 89 | --------------------------------------------------------------------------------