├── .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 | key |
287 | type |
288 | required? |
289 | description |
290 | example |
291 |
292 | storage |
293 | string |
294 | no |
295 | type 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 | |
304 | "file" |
305 |
306 |
307 | schema |
308 | object |
309 | required |
310 | DB schema |
311 | (see SCHEMA JSON) |
312 |
313 | reset |
314 | boolean |
315 | no (default false) |
316 | if true, reset db with the given schema. |
317 | true |
318 |
319 | name |
320 | string |
321 | no (default : the same as uniqId) |
322 | the name of the db |
323 | appname_test |
324 |
325 | autosave |
326 | boolean |
327 | no (default false) |
328 | whether to auto-saving or not |
329 | true |
330 |
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 | key |
348 | type |
349 | description |
350 | example |
351 |
352 | (columnName) |
353 | columnDescription |
354 | column 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 | |
363 | age: "num" |
364 |
365 |
366 | $indexes |
367 | Array |
368 | list of indexes. child arrays are lists of columns to make an index.
369 | If string given, converted as array with the value
370 | |
371 | [["name"], ["firstName", "lastName"]] |
372 |
373 | $uniques |
374 | Array |
375 | (the same as $indexes, but this means unique index) |
376 | [["name", "pass"]] |
377 |
378 | $classes |
379 | Array |
380 | (the same as $indexes, but this means classified index) |
381 | "type_num" |
382 |
383 |
384 |
385 | **column description**
386 |
387 | example |
388 | description |
389 |
390 |
{type: "str"} |
391 | type is string.
392 | type must be one of ["str", "num", "bool", (columnName)]
393 | |
394 |
395 | {type: "str", required: false} |
396 | type is string, and if not given, null is set.
397 | required option is false by default
398 | |
399 |
400 | {type: "bool", _default: true} |
401 | type is boolean, and if not given, true is set. |
402 |
403 | {type: "num", required: true} |
404 | type is number, and if not given, an exception is thrown. |
405 |
406 | "str" |
407 | type is string, and not required. |
408 |
409 | "num" |
410 | type is number, and not required. |
411 |
412 | "bool" |
413 | type is boolean, and not required. |
414 |
415 | true |
416 | type is string, and required. |
417 |
418 | false |
419 | type is string, and not required. |
420 |
421 | 1 |
422 | type is number, and required. |
423 |
424 | 0 |
425 | type is number, and not required. |
426 |
427 | "on" |
428 | type is boolean, and default value is true. |
429 |
430 | "off" |
431 | type is boolean, and default value is false. |
432 |
433 | {type: tableName} |
434 | 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 | |
439 |
440 | {type: tableName, required: false} |
441 | type is the instance of a record in tableName and not required.
442 | |
443 |
444 | tableName |
445 | type is the instance of a record in tableName and required.
446 | |
447 |
448 |
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 | key |
468 | type |
469 | required? |
470 | description |
471 | example |
472 |
473 | force |
474 | boolean |
475 | no (default : false) |
476 | if true, overrides already-existing database of the same uniqId.
477 | otherwise throws an error. |
478 | true |
479 |
480 | name |
481 | string |
482 | no |
483 | the name of the db. If undefined, the imported name is used. |
484 | appname_2 |
485 |
486 | autosave |
487 | boolean |
488 | no |
489 | whether to auto-saving or not. If undefineed, the imported autosave preference is used. |
490 | true |
491 |
492 | storage |
493 | string |
494 | no |
495 | type of external storages. see options of JSRel.use().
496 | If undefined, the imported storage preference is used.
497 | |
498 | "file" |
499 |
500 |
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 | event name |
551 | emitted when |
552 | arguments to be passed |
553 |
554 | ins |
555 | data are inserted |
556 |
557 |
558 | - **tableName** : table name
559 |
- **insObj** : inserted object (with id)
560 |
561 | |
562 |
563 |
564 | ins:{tablename} |
565 | data are inserted into {tablename} |
566 |
567 |
568 | - **insObj** : inserted object (with id)
569 |
570 | |
571 |
572 |
573 | upd |
574 | data are updated |
575 |
576 |
577 | - **tableName** : table name
578 |
- **updObj** : updated object
579 |
- **oldObj** : object before updating
580 |
- **updColumns** :updated columns (Array)
581 |
582 | |
583 |
584 |
585 | upd:{tablename} |
586 | data are updated in {tablename} |
587 |
588 |
589 | - **updObj** : updated object
590 |
- **oldObj** : object before updating
591 |
- **updColumns** :updated columns (Array)
592 |
593 | |
594 |
595 |
596 | del |
597 | data are deleted |
598 |
599 |
600 | - **tableName** : table name
601 |
- **delObj** : deleted object
602 |
603 | |
604 |
605 |
606 | del:{tablename} |
607 | data are deleted in {tablename} |
608 |
609 |
610 | - **delObj** : deleted object
611 |
612 | |
613 |
614 |
615 | save:start |
616 | at the start of jsrel.save() |
617 |
618 |
619 | - **origin** : result of db.origin()
620 |
621 | |
622 |
623 |
624 | save:end |
625 | at the end of jsrel.save() |
626 |
627 |
628 | - **data** : saved data
629 |
630 | |
631 |
632 |
633 |
634 |
635 |
636 | #### options ####
637 |
638 | option name |
639 | type |
640 | description |
641 | default |
642 |
643 | unshift |
644 | boolean |
645 | registers a function to the top of the list |
646 | false |
647 |
648 |
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 | option name |
666 | type |
667 | description |
668 | default |
669 | example |
670 |
671 | noschema |
672 | boolean |
673 | if true, schema SQLs (create statements) are not generated. |
674 | null |
675 | true |
676 |
677 |
678 | db |
679 | boolean or string |
680 | if 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 | | null |
683 | true |
684 |
685 |
686 | nodrop |
687 | boolean |
688 | if true, drop statements are not generated. |
689 | null |
690 | true |
691 |
692 |
693 | nodata |
694 | boolean |
695 | if true, data SQLs (insert statements) are not generated. |
696 | null |
697 | true |
698 |
699 |
700 | type |
701 | string |
702 | type of RDBs. Currently, "mysql" is only tested. |
703 | "mysql" |
704 | "mysql" |
705 |
706 |
707 | engine |
708 | string |
709 | MySQL engine (only enabled when options.type is "mysql") |
710 | "InnoDB" |
711 | "MyISAM" |
712 |
713 |
714 | rails (unstable) |
715 | boolean |
716 | if true, rails-like date format (created_at, inserted_at) is output. |
717 | null |
718 | true |
719 |
720 |
721 |
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 | example |
982 | description |
983 |
984 |
{name: "shinout"} |
985 | name must be equal to "shinout" |
986 |
987 | {name: ["shinout", "nishiko"]} |
988 | name must be equal to "shinout" or "nishiko" |
989 |
990 | {name: {like$: "shin"}} |
991 | name must be like "shin%" |
992 |
993 | {name: {$like: "inout"}} |
994 | name must be like "%inout" |
995 |
996 | {name: [{$like: "inout"}, {equal: "nishiko"}] } |
997 | name must be like "%inout" OR equals "nishiko" |
998 |
999 | {name: {$like: "inout", equal: "nishiko"} } |
1000 | name must be like "%inout" AND equals "nishiko" |
1001 |
1002 | {age: {gt: 24} } |
1003 | age must be greater than 24 |
1004 |
1005 | {age: {gt: 24, le: 40} } |
1006 | age must be greater than 24 and less equal 40 |
1007 |
1008 | {age: [{ge: 24}, {lt: 40}] } |
1009 | age must be greater equal 24 or less than 40 |
1010 |
1011 | {country: {$in: ["Japan", "Korea"] } |
1012 | country must be one of "Japan", "Korea" (as "in" is a reserved word in JavaScript, used "$in" instead.) |
1013 |
1014 | {name: "shinout", age : {ge: 70 } |
1015 | must returns empty until shinout becomes 70 |
1016 |
1017 |
1018 |
1019 |
1020 | **options** is as follows.
1021 |
1022 |
1023 | key |
1024 | type |
1025 | description |
1026 | example |
1027 |
1028 | order |
1029 | mixed |
1030 | see order description |
1031 | { name: "asc" } |
1032 |
1033 |
1034 | limit |
1035 | int |
1036 | the end position of the data |
1037 | 20 |
1038 |
1039 | offset |
1040 | int |
1041 | offset of the results |
1042 | 10 |
1043 |
1044 | join |
1045 | mixed |
1046 | see join description |
1047 | {records.scene: {title : {like$: "ABC"} } |
1048 |
1049 | select |
1050 | string (one of column names) |
1051 | get list of selected columns instead of objects |
1052 | "title" |
1053 |
1054 | select |
1055 | array (list of column names) |
1056 | get list of object which contains the given columns instead of all columns |
1057 | ["name", "age"] |
1058 |
1059 | explain |
1060 | object |
1061 | put searching information to the given object |
1062 | {} |
1063 |
1064 |
1065 |
1066 | #### order description ####
1067 |
1068 | example |
1069 | description |
1070 |
1071 |
"age" |
1072 | order by age asc |
1073 |
1074 | {age: "desc"} |
1075 | order by age desc |
1076 |
1077 | {age: "desc", name: "asc"} |
1078 | order by age desc, name asc |
1079 |
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 | id | name |
1095 | 1 | mindia |
1096 | 2 | ZZZ |
1097 |
1098 |
1099 | user
1100 |
1101 | id | name | age | group |
1102 | 1 | shinout | 25 | 1 |
1103 | 2 | nishiko | 28 | 1 |
1104 | 3 | xxx | 39 | 2 |
1105 |
1106 |
1107 | card
1108 |
1109 | id | title | body |
1110 | 1 | rabbit | it jumps! |
1111 | 2 | pot | a tiny yellow magic pot |
1112 | 3 | PC | calculating... |
1113 |
1114 |
1115 | user_card
1116 |
1117 | id | user | card |
1118 | 1 | 1 | 1 |
1119 | 2 | 2 | 1 |
1120 | 3 | 1 | 2 |
1121 | 4 | 2 | 3 |
1122 | 5 | 3 | 3 |
1123 |
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 | No. |
1134 | JOIN_VALUE |
1135 | description |
1136 | result |
1137 |
1138 |
1139 | 1 |
1140 | "group" |
1141 | get "group" column as object |
1142 | [{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}] |
1143 |
1144 |
1145 |
1146 | 2 |
1147 | {group : true} |
1148 | get "group" column as object (the same as sample1) |
1149 | [{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}] |
1150 |
1151 |
1152 |
1153 | 3 |
1154 | true |
1155 | get all the related columns as object |
1156 | [{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}] |
1157 |
1158 |
1159 |
1160 | 4 |
1161 | {group : {name: {like$: "mind"}}} |
1162 | get "group" column as object whose name starts at "mind" |
1163 | [{id: 1, name: "shinout", age: 25, group_id: 1, group: {id: 1, name: "mindia"}}] |
1164 |
1165 |
1166 |
1167 | 5 |
1168 | {group : {name: "ZZZ"}} |
1169 | get "group" column as object whose name is equal to "ZZZ" |
1170 | [] // empty |
1171 |
1172 |
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 | No. |
1182 | JOIN_VALUE |
1183 | description |
1184 | result |
1185 |
1186 |
1187 | 6 |
1188 | "user.group" |
1189 | get "user" table objects (setting the related column in "user" table) |
1190 | [{id: 1, name: "mindia", "user.group": [{id: 1, name: "shinout", age: 25}, {id: 2, name: "nishiko", age: 28}]}] |
1191 |
1192 |
1193 |
1194 | 7 |
1195 | "user" |
1196 | get "user" table objects (if related column is obvious) |
1197 | [{id: 1, name: "mindia", "user": [{id: 1, name: "shinout", age: 25}, {id: 2, name: "nishiko", age: 28}]}] |
1198 |
1199 |
1200 |
1201 | 8 |
1202 | {"user.group" : true } |
1203 | get "user" table objects (the same as sample6) |
1204 | [{id: 1, name: "mindia", "user.group": [{id: 1, name: "shinout", age: 25}, {id: 2, name: "nishiko", age: 28}]}] |
1205 |
1206 |
1207 |
1208 | 9 |
1209 | {"user.group" : {age : {gt: 27}} } |
1210 | get "user" table objects with age greater than 27 |
1211 | [{id: 1, name: "mindia", "user.group": [{id: 2, name: "nishiko", age: 28}]}] |
1212 |
1213 |
1214 |
1215 | 10 |
1216 | {"user.group" : {age : {gt: 27}, as: "users"} } |
1217 | get "user" table objects with age greater than 27, with alias name "users" |
1218 | [{id: 1, name: "mindia", "users": [{id: 2, name: "nishiko", age: 28}]}] |
1219 |
1220 |
1221 |
1222 | 11 |
1223 | {"user.group" : {where : {age : {gt: 27}}, as: "users"} } |
1224 | get "user" table objects with age greater than 27, with alias name "users" (the canonical expression of sample9) |
1225 | [{id: 1, name: "mindia", "users": [{id: 2, name: "nishiko", age: 28}]}] |
1226 |
1227 |
1228 |
1229 | 12 |
1230 | {user : {age : {gt: 27}, as: "users"} } |
1231 | get "user" table objects with age greater than 27, with alias name "users" |
1232 | [{id: 1, name: "mindia", "users": [{id: 2, name: "nishiko", age: 28}]}] |
1233 |
1234 |
1235 |
1236 | 13 |
1237 | {user : {age : {gt: 47}, outer: true} } |
1238 | outer joining. Records containing Empty 1:N subqueries can be remained with the column filled with null. |
1239 | [{id: 1, name: "mindia", "user": null}] |
1240 |
1241 |
1242 |
1243 | 13 |
1244 | {user : {age : {gt: 47}, outer: "array"} } |
1245 | outer joining. Records containing Empty 1:N subqueries can be remained with the column filled with empty array. |
1246 | [{id: 1, name: "mindia", "user": [] }] |
1247 |
1248 |
1249 |
1250 |
1251 |
1252 | **Fetching N:M related objects**
1253 |
1254 | var result = db.table('user').find({name: "shinout"}, {join: JOIN_VALUE});
1255 |
1256 |
1257 |
1258 | 15 |
1259 | {"card": {via: "user_card"} } |
1260 | get "card" related through "user_card" |
1261 | [{id: 1, name: "shinout", "card": [ {id:1, ...}, {id: 3, ...}] }] |
1262 |
1263 |
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 |
--------------------------------------------------------------------------------