├── data ├── names.tsv ├── nokeys.tsv ├── users.tsv ├── users.csv ├── books.tsv ├── user_books.tsv ├── benchmark.png ├── nicknames.tsv ├── passwords.tsv └── benchmark.rb ├── Gemfile ├── lib ├── active_tsv │ ├── version.rb │ ├── condition.rb │ ├── errors.rb │ ├── where_chain.rb │ ├── querying.rb │ ├── querying_test.rb │ ├── where_chain_test.rb │ ├── ordering.rb │ ├── reflection.rb │ ├── base_benchmark_test.rb │ ├── base.rb │ ├── reflection_test.rb │ ├── base_test.rb │ ├── relation.rb │ └── relation_test.rb ├── active_csv.rb ├── active_tsv.rb └── active_csv_test.rb ├── .travis.yml ├── Rakefile ├── .gitignore ├── bin ├── setup └── console ├── active_tsv.gemspec ├── LICENSE.txt └── README.md /data/names.tsv: -------------------------------------------------------------------------------- 1 | name 2 | foo 3 | bar 4 | baz 5 | -------------------------------------------------------------------------------- /data/nokeys.tsv: -------------------------------------------------------------------------------- 1 | 1 2 3 2 | 2 3 4 3 | 3 4 5 4 | -------------------------------------------------------------------------------- /data/users.tsv: -------------------------------------------------------------------------------- 1 | id name age 2 | 1 "ksss" 30 3 | 2 foo 29 4 | 3 bar 30 5 | -------------------------------------------------------------------------------- /data/users.csv: -------------------------------------------------------------------------------- 1 | id,name,age 2 | 1,"ksss",30 3 | 2,"foo",29 4 | 3,"bar",30 5 | -------------------------------------------------------------------------------- /data/books.tsv: -------------------------------------------------------------------------------- 1 | id title 2 | 1 Good book 3 | 2 Greate book 4 | 3 Perfect book 5 | -------------------------------------------------------------------------------- /data/user_books.tsv: -------------------------------------------------------------------------------- 1 | id user_id book_id 2 | 1 1 1 3 | 2 1 2 4 | 3 1 3 5 | 4 2 2 6 | -------------------------------------------------------------------------------- /data/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksss/active_tsv/HEAD/data/benchmark.png -------------------------------------------------------------------------------- /data/nicknames.tsv: -------------------------------------------------------------------------------- 1 | id user_id nickname 2 | 1 1 yuki 3 | 2 1 kuri 4 | 3 1 k 5 | 4 2 f 6 | -------------------------------------------------------------------------------- /data/passwords.tsv: -------------------------------------------------------------------------------- 1 | id user_id password 2 | 1 1 abcdefg 3 | 2 2 hijklmn 4 | 3 3 opqrstu 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in active_tsv.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/active_tsv/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveTsv 4 | VERSION = "0.3.2" 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.6 4 | - 2.3.3 5 | - 2.4.0 6 | notifications: 7 | email: false 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | task :test do 4 | sh "bundle ex rgot -v --bench ." 5 | end 6 | task default: :test 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/active_csv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_tsv' 4 | 5 | module ActiveCsv 6 | class Base < ActiveTsv::Base 7 | SEPARATER = "," 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/active_tsv/condition.rb: -------------------------------------------------------------------------------- 1 | module ActiveTsv 2 | class Condition < Struct.new(:values) 3 | class Equal < Condition 4 | end 5 | 6 | class NotEqual < Condition 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/active_tsv/errors.rb: -------------------------------------------------------------------------------- 1 | module ActiveTsv 2 | ActiveTsvError = Class.new(StandardError) 3 | RecordNotFound = Class.new(ActiveTsvError) 4 | StatementInvalid = Class.new(ActiveTsvError) 5 | UnknownAttributeError = Class.new(NoMethodError) 6 | end 7 | -------------------------------------------------------------------------------- /lib/active_tsv/where_chain.rb: -------------------------------------------------------------------------------- 1 | module ActiveTsv 2 | class WhereChain 3 | def initialize(relation) 4 | @relation = relation 5 | end 6 | 7 | def not(condition) 8 | @relation.dup.tap do |r| 9 | r.where_values << Condition::NotEqual.new(condition) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/active_tsv/querying.rb: -------------------------------------------------------------------------------- 1 | module ActiveTsv 2 | module Querying 3 | METHODS = %i(find first last take where count order group pluck minimum maximum) 4 | METHODS.each do |m| 5 | module_eval <<-DEFINE_METHOD, __FILE__, __LINE__ + 1 6 | def #{m}(*args, &block) 7 | all.#{m}(*args, &block) 8 | end 9 | DEFINE_METHOD 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "active_tsv" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /lib/active_tsv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'csv' 4 | 5 | require "active_support/inflector" 6 | 7 | require "active_tsv/querying" 8 | require "active_tsv/reflection" 9 | require "active_tsv/relation" 10 | require "active_tsv/where_chain" 11 | require "active_tsv/condition" 12 | require "active_tsv/ordering" 13 | require "active_tsv/errors" 14 | require "active_tsv/base" 15 | require "active_tsv/version" 16 | -------------------------------------------------------------------------------- /lib/active_tsv/querying_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_tsv' 2 | 3 | module ActiveTsvQueryingTest 4 | class User < ActiveTsv::Base 5 | self.table_path = "data/users.tsv" 6 | end 7 | 8 | def test_delegate(t) 9 | r = User.all 10 | ActiveTsv::Querying::METHODS.each do |m| 11 | unless r.respond_to?(m) 12 | t.error("expect delegate to Relation `#{m}' but nothing") 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/active_csv_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_csv' 2 | 3 | module ActiveCsvTest 4 | class User < ActiveCsv::Base 5 | self.table_path = "data/users.csv" 6 | end 7 | 8 | def test_first(t) 9 | u = User.first 10 | unless User === u 11 | t.error("return value was break") 12 | end 13 | 14 | { 15 | id: "1", 16 | name: "ksss", 17 | age: "30", 18 | }.each do |k, v| 19 | unless u[k] == v 20 | t.error("User##{k} expect #{v} got #{u[k]}") 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/active_tsv/where_chain_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_tsv' 2 | 3 | module ActiveTsvWhereChainTest 4 | class User < ActiveTsv::Base 5 | self.table_path = "data/users.tsv" 6 | end 7 | 8 | def test_not(t) 9 | r = User.where.not(age: 29) 10 | unless ActiveTsv::Relation === r 11 | t.error("break return value #{r}") 12 | end 13 | a = r.to_a 14 | unless a.length == 2 15 | t.error("expect length 2 got #{a.length}") 16 | end 17 | 18 | u = r.where.not(name: "ksss").first 19 | unless u && u.name == "bar" && u.age == "30" 20 | t.error("expect \"bar\" got #{u.inspect}") 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/active_tsv/ordering.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveTsv 4 | class Ordering < Struct.new(:column) 5 | VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"] 6 | 7 | class Ascending < Ordering 8 | def to_i 9 | 1 10 | end 11 | 12 | def ascending? 13 | true 14 | end 15 | 16 | def descending? 17 | false 18 | end 19 | end 20 | 21 | class Descending < Ordering 22 | def to_i 23 | -1 24 | end 25 | 26 | def ascending? 27 | false 28 | end 29 | 30 | def descending? 31 | true 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /active_tsv.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'active_tsv/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "active_tsv" 8 | spec.version = ActiveTsv::VERSION 9 | spec.authors = ["ksss"] 10 | spec.email = ["co000ri@gmail.com"] 11 | 12 | spec.summary = "A Class of Active record pattern for TSV/CSV" 13 | spec.description = "A Class of Active record pattern for TSV/CSV" 14 | spec.homepage = "https://github.com/ksss/active_tsv" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{_test.rb}) } 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_runtime_dependency "activesupport" 21 | spec.add_development_dependency "bundler", "~> 1.11" 22 | spec.add_development_dependency "rake", "~> 10.0" 23 | spec.add_development_dependency "rgot" 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 ksss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/active_tsv/reflection.rb: -------------------------------------------------------------------------------- 1 | module ActiveTsv 2 | module Reflection 3 | def has_many(name, through: nil) 4 | if through 5 | class_eval <<-CODE, __FILE__, __LINE__ + 1 6 | def #{name} 7 | #{name.to_s.classify}.where( 8 | #{name.to_s.classify}.primary_key => #{through}.pluck(:#{name.to_s.singularize.underscore}_id) 9 | ) 10 | end 11 | CODE 12 | else 13 | class_eval <<-CODE, __FILE__, __LINE__ + 1 14 | def #{name} 15 | #{name.to_s.singularize.classify}.where( 16 | #{self.name.underscore}_id: self[self.class.primary_key] 17 | ) 18 | end 19 | CODE 20 | end 21 | end 22 | 23 | def has_one(name) 24 | class_eval <<-CODE, __FILE__, __LINE__ + 1 25 | def #{name} 26 | #{name.to_s.singularize.classify}.where( 27 | #{self.name.underscore}_id: self[self.class.primary_key] 28 | ).first 29 | end 30 | CODE 31 | end 32 | 33 | def belongs_to(name) 34 | class_eval <<-CODE, __FILE__, __LINE__ + 1 35 | def #{name} 36 | #{name.to_s.classify}.where(self.class.primary_key => self["#{name}_id"]).first 37 | end 38 | CODE 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /data/benchmark.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require 'active_tsv' 4 | require 'tempfile' 5 | require 'active_hash' 6 | require 'csv' 7 | require 'objspace' 8 | require 'stringio' 9 | 10 | module ActiveHashTsv 11 | class Base < ActiveFile::Base 12 | SEPARATER = "\t" 13 | extend ActiveFile::HashAndArrayFiles 14 | class << self 15 | def load_file 16 | raw_data 17 | end 18 | 19 | def extension 20 | "tsv" 21 | end 22 | 23 | private 24 | 25 | def load_path(path) 26 | data = [] 27 | CSV.open(path, col_sep: self::SEPARATER) do |csv| 28 | column_names = csv.gets.map(&:to_sym) 29 | while line = csv.gets 30 | data << column_names.zip(line).to_h 31 | end 32 | end 33 | data 34 | end 35 | end 36 | end 37 | end 38 | 39 | def open_csv_with_temp_table(n) 40 | headers = [*'a'..'j'] 41 | Tempfile.create(["", ".tsv"]) do |f| 42 | f.puts headers.join("\t") 43 | n.times do |i| 44 | f.puts [*1..(headers.length)].map{ |j| i * j }.join("\t") 45 | end 46 | f.close 47 | yield f.path 48 | end 49 | end 50 | io = StringIO.new 51 | $stdout = io 52 | def b 53 | GC.start 54 | before = ObjectSpace.memsize_of_all 55 | realtime = Benchmark.realtime { 56 | yield 57 | } 58 | GC.start 59 | mem = (ObjectSpace.memsize_of_all - before).to_f 60 | [realtime, mem] 61 | end 62 | puts "title\tActiveHash\tActiveTsv\ttActiveHash\ttActiveTsv" 63 | ns = [100, 200, 300, 400, 500] 64 | ns.each do |n| 65 | open_csv_with_temp_table(n) do |path| 66 | hr, hm = b { 67 | h = Class.new(ActiveHashTsv::Base) do 68 | set_root_path File.dirname(path) 69 | set_filename File.basename(path).sub(/\..*/, '') 70 | end 71 | h.all.each {} 72 | } 73 | tr, tm = b { 74 | t = Class.new(ActiveTsv::Base) do 75 | self.table_path = path 76 | end 77 | t.all.each {} 78 | } 79 | puts sprintf("%d\t%0.5f\t%0.5f\t%d\t%d", n, hr, tr, hm, tm) 80 | 81 | hr, hm = b { 82 | h = Class.new(ActiveHashTsv::Base) do 83 | set_root_path File.dirname(path) 84 | set_filename File.basename(path).sub(/\..*/, '') 85 | end 86 | h.where(a: '10').first 87 | } 88 | 89 | tr, tm = b { 90 | t = Class.new(ActiveTsv::Base) do 91 | self.table_path = path 92 | end 93 | t.where(a: '10').first 94 | } 95 | puts sprintf("%d\t%0.5f\t%0.5f\t%d\t%d", n, hr, tr, hm, tm) 96 | end 97 | end 98 | $stdout = STDOUT 99 | io.rewind 100 | puts io.gets 101 | lines = io.each_line.to_a 102 | 2.times do |i| 103 | puts lines.values_at(i, i+2, i+4, i+6, i+8) 104 | end 105 | -------------------------------------------------------------------------------- /lib/active_tsv/base_benchmark_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_tsv' 2 | require 'benchmark' 3 | require 'tempfile' 4 | 5 | module ActiveTsvBenchmarkTest 6 | def run_with_temp_table(n) 7 | Tempfile.create(["", ".tsv"]) do |f| 8 | f.puts [*'a'..'z'].join("\t") 9 | n.times do |i| 10 | f.puts [*1..26].map{ |j| (i+1) * j }.join("\t") 11 | end 12 | 13 | f.close 14 | 15 | bench_klass = Class.new(ActiveTsv::Base) do 16 | self.table_path = f.path 17 | end 18 | yield bench_klass, n / 2 19 | end 20 | end 21 | 22 | def benchmark_where(b) 23 | run_with_temp_table(10000) do |bench_klass, n| 24 | b.reset_timer 25 | i = 0 26 | while i < b.n 27 | bench_klass.where(a: 1 * n, b: 2 * n, c: 3 * n) 28 | .where(d: 4 * n, e: 5 * n, f: 6 * n) 29 | .where(g: 7 * n, h: 8 * n, i: 9 * n) 30 | .where(j: 10 * n, k: 11 * n, l: 12 * n) 31 | .where(m: 13 * n, n: 14 * n, o: 15 * n) 32 | .where(p: 16 * n, q: 17 * n, r: 18 * n) 33 | .where(s: 19 * n, t: 20 * n, u: 21 * n) 34 | .where(v: 22 * n, w: 23 * n, x: 24 * n) 35 | .where(y: 25 * n, z: 26 * n).first 36 | i += 1 37 | end 38 | end 39 | end 40 | 41 | def benchmark_to_a(b) 42 | run_with_temp_table(1000) do |bench_klass, n| 43 | b.reset_timer 44 | i = 0 45 | while i < b.n 46 | bench_klass.where(a: 1 * n).to_a 47 | i += 1 48 | end 49 | end 50 | end 51 | 52 | def benchmark_relation_first(b) 53 | run_with_temp_table(5000) do |bench_klass, n| 54 | b.reset_timer 55 | i = 0 56 | while i < b.n 57 | bench_klass.where(a: 1).first 58 | i += 1 59 | end 60 | end 61 | end 62 | 63 | def benchmark_last(b) 64 | run_with_temp_table(5000) do |bench_klass, n| 65 | b.reset_timer 66 | i = 0 67 | while i < b.n 68 | bench_klass.last 69 | i += 1 70 | end 71 | end 72 | end 73 | 74 | def benchmark_active_tsv_each(b) 75 | run_with_temp_table(1000) do |bench_klass, n| 76 | r = bench_klass.all 77 | b.reset_timer 78 | i = 0 79 | while i < b.n 80 | r.each {} 81 | i += 1 82 | end 83 | end 84 | end 85 | 86 | def benchmark_csv_each(b) 87 | run_with_temp_table(1000) do |bench_klass, n| 88 | b.reset_timer 89 | i = 0 90 | while i < b.n 91 | CSV.open(bench_klass.table_path, col_sep: "\t".freeze).each {} 92 | i += 1 93 | end 94 | end 95 | end 96 | 97 | def benchmark_order(b) 98 | run_with_temp_table(1000) do |bench_klass, n| 99 | b.reset_timer 100 | i = 0 101 | while i < b.n 102 | bench_klass.order(:a).to_a 103 | i += 1 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveTsv 2 | 3 | [![Build Status](https://travis-ci.org/ksss/active_tsv.svg?branch=master)](https://travis-ci.org/ksss/active_tsv) 4 | 5 | A Class of Active record pattern for TSV/CSV 6 | 7 | ## Usage 8 | 9 | data/users.tsv 10 | 11 | ```tsv 12 | id name age 13 | 1 ksss 30 14 | 2 foo 29 15 | 3 bar 30 16 | ``` 17 | 18 | data/nicknames.tsv 19 | 20 | ```tsv 21 | id user_id nickname 22 | 1 1 yuki 23 | 2 1 kuri 24 | 3 1 k 25 | 4 2 f 26 | ``` 27 | 28 | ```ruby 29 | require 'active_tsv' 30 | 31 | class User < ActiveTsv::Base 32 | self.table_path = "data/users.tsv" # required 33 | # self.encoding = Encoding::Shift_JIS # optional 34 | # self.primary_key = "uid" # optional 35 | has_many :nicknames 36 | end 37 | 38 | class Nickname < ActiveTsv::Base 39 | self.table_path = "data/nicknames.tsv" 40 | belongs_to :user 41 | end 42 | 43 | User.all 44 | => #, #, #]> 45 | User.all.to_a 46 | => [#, #, #] 47 | 48 | User.first 49 | #=> # 50 | User.last 51 | #=> # 52 | 53 | User.where(age: 30).each do |user| 54 | user.name #=> "ksss", "bar" 55 | end 56 | 57 | User.where(age: 30).to_a 58 | #=> [#, #] 59 | 60 | User.where(age: 30).last 61 | #=> # 62 | 63 | User.where(age: 30).where(name: "ksss").first 64 | #=> # 65 | 66 | User.where(id: [1, 2]).to_a 67 | #=> [#, #] 68 | 69 | User.where.not(name: "ksss").first 70 | #=> # 71 | 72 | User.group(:age).count 73 | #=> {"30"=>2, "29"=>1} 74 | 75 | User.order(:name).to_a 76 | #=> [#, #, #] 77 | 78 | User.order(name: :desc).to_a 79 | => [#, #, #] 80 | 81 | User.first.nicknames 82 | #=> #, #, #]> 83 | 84 | Nickname.last.user 85 | #=> # 86 | ``` 87 | 88 | Also Supported **CSV**. 89 | 90 | ```ruby 91 | require 'active_csv' 92 | class User < ActiveCsv::Base 93 | self.table_path = "data/users.csv" 94 | end 95 | ``` 96 | 97 | ## Goal 98 | 99 | Support all methods of ActiveRecord 100 | 101 | ## Installation 102 | 103 | Add this line to your application's Gemfile: 104 | 105 | ```ruby 106 | gem 'active_tsv' 107 | ``` 108 | 109 | And then execute: 110 | 111 | $ bundle 112 | 113 | Or install it yourself as: 114 | 115 | $ gem install active_tsv 116 | 117 | ## License 118 | 119 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 120 | -------------------------------------------------------------------------------- /lib/active_tsv/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveTsv 4 | # @example 5 | # class User < ActiveTsv::Base 6 | # self.table_path = "table/product_masters.tsv" 7 | # end 8 | class Base 9 | SEPARATER = "\t" 10 | DEFAULT_PRIMARY_KEY = "id" 11 | 12 | class << self 13 | include Querying 14 | include Reflection 15 | 16 | attr_reader :table_path 17 | 18 | def table_path=(path) 19 | reload(path) 20 | end 21 | 22 | def reload(path) 23 | if @column_names 24 | column_names.each do |k| 25 | remove_method(k) 26 | remove_method("#{k}=") 27 | end 28 | end 29 | 30 | @column_names = nil 31 | @custom_column_name = false 32 | @table_path = path 33 | column_names.each do |k| 34 | define_method(k) { @attrs[k] } 35 | define_method("#{k}=") { |v| @attrs[k] = v } 36 | end 37 | end 38 | 39 | def all 40 | Relation.new(self) 41 | end 42 | 43 | def scope(name, proc) 44 | define_singleton_method(name, &proc) 45 | end 46 | 47 | def open(&block) 48 | CSV.open(table_path, "r:#{encoding}:UTF-8", col_sep: self::SEPARATER, &block) 49 | end 50 | 51 | def column_names 52 | @column_names ||= begin 53 | @custom_column_name = false 54 | open { |csv| csv.gets } 55 | end 56 | end 57 | 58 | def column_names=(names) 59 | @custom_column_name = true 60 | column_names = names.map(&:to_s) 61 | column_names.each do |k| 62 | define_method(k) { @attrs[k] } 63 | define_method("#{k}=") { |v| @attrs[k] = v } 64 | end 65 | @column_names = column_names 66 | end 67 | 68 | def custom_column_name? 69 | @custom_column_name 70 | end 71 | 72 | def primary_key 73 | @primary_key ||= DEFAULT_PRIMARY_KEY 74 | end 75 | 76 | attr_writer :primary_key 77 | 78 | def encoding 79 | @encoding ||= Encoding::UTF_8 80 | end 81 | 82 | def encoding=(enc) 83 | case enc 84 | when String 85 | @encoding = Encoding.find(enc) 86 | when Encoding 87 | @encoding = enc 88 | else 89 | raise ArgumentError, "#{enc.class} dose not support" 90 | end 91 | end 92 | end 93 | 94 | def initialize(attrs = {}) 95 | case attrs 96 | when Hash 97 | h = {} 98 | self.class.column_names.each do |name| 99 | h[name] = nil 100 | end 101 | attrs.each do |name, v| 102 | unless respond_to?("#{name}=") 103 | raise UnknownAttributeError, "unknown attribute '#{name}' for #{self.class}." 104 | end 105 | h[name.to_s] = v 106 | end 107 | @attrs = h 108 | when Array 109 | @attrs = self.class.column_names.zip(attrs).to_h 110 | else 111 | raise ArgumentError, "#{attrs.class} is not supported value" 112 | end 113 | end 114 | 115 | def inspect 116 | "#<#{self.class} #{@attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>" 117 | end 118 | 119 | def [](key) 120 | @attrs[key.to_s] 121 | end 122 | 123 | def []=(key, value) 124 | @attrs[key.to_s] = value 125 | end 126 | 127 | def attributes 128 | @attrs.dup 129 | end 130 | 131 | def ==(other) 132 | super || other.instance_of?(self.class) && @attrs == other.attributes 133 | end 134 | alias eql? == 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/active_tsv/reflection_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_tsv' 2 | 3 | class User < ActiveTsv::Base 4 | self.table_path = "data/users.tsv" 5 | has_many :nicknames 6 | has_many :nothings 7 | has_one :password 8 | has_one :nothing 9 | 10 | has_many :user_books 11 | has_many :books, through: :user_books 12 | end 13 | 14 | class Nickname < ActiveTsv::Base 15 | self.table_path = "data/nicknames.tsv" 16 | belongs_to :user 17 | belongs_to :nothing 18 | end 19 | 20 | class Password < ActiveTsv::Base 21 | self.table_path = "data/passwords.tsv" 22 | belongs_to :user 23 | end 24 | 25 | class Book < ActiveTsv::Base 26 | self.table_path = "data/books.tsv" 27 | has_many :user_books 28 | has_many :users, through: :user_books 29 | end 30 | 31 | class UserBook < ActiveTsv::Base 32 | self.table_path = "data/user_books.tsv" 33 | belongs_to :user 34 | belongs_to :book 35 | end 36 | 37 | module ActiveTsvReflectionTest 38 | def test_s_has_many(t) 39 | [ 40 | [User.first, %w(yuki kuri k)], 41 | [User.where(name: "foo").first, ["f"]], 42 | [User.where(name: "bar").first, []], 43 | ].each do |user, expect| 44 | r = user.nicknames 45 | unless ActiveTsv::Relation === r 46 | t.error("`#{user}.nicknames` return value broken") 47 | end 48 | unless r.all? { |i| i.instance_of?(Nickname) } 49 | t.error("broken reflection") 50 | end 51 | unless r.to_a.map(&:nickname) == expect 52 | t.error("expect #{expect} got #{r.to_a.map(&:nickname)}") 53 | end 54 | end 55 | 56 | begin 57 | User.first.nothings 58 | rescue NameError => e 59 | unless e.message == "uninitialized constant User::Nothing" 60 | t.error("Unexpected error message '#{e.message}'") 61 | end 62 | else 63 | t.error("expect raise NameError") 64 | end 65 | end 66 | 67 | def test_s_has_many_with_through(t) 68 | ksss, foo, bar = *User.all.to_a 69 | 70 | [ 71 | [ksss, ["Good book", "Greate book", "Perfect book"]], 72 | [foo, ["Greate book"]], 73 | [bar, []], 74 | ].each do |user, expect| 75 | r = user.books 76 | unless ActiveTsv::Relation === r 77 | t.error("return value was broken") 78 | end 79 | unless r.map(&:title) == expect 80 | t.error("expect #{expect} got #{r.map(&:title)}") 81 | end 82 | end 83 | end 84 | 85 | def test_s_has_one(t) 86 | [ 87 | [User.first, Password.first], 88 | [User.last, Password.last], 89 | ].each do |user, pass| 90 | unless Password === pass 91 | t.error("Unexpected instance #{pass}") 92 | end 93 | unless user.id == pass.user_id 94 | t.error("user.id(#{user.id}) != pass.user_id(#{pass.user_id})") 95 | end 96 | unless user.password.password == pass.password 97 | t.error("unexpected instance '#{user}' and '#{pass}'") 98 | end 99 | end 100 | 101 | begin 102 | User.first.nothing 103 | rescue NameError => e 104 | unless e.message == "uninitialized constant User::Nothing" 105 | t.error("Unexpected error message '#{e.message}'") 106 | end 107 | else 108 | t.error("expect raise a NameError") 109 | end 110 | end 111 | 112 | def test_s_belongs_to(t) 113 | u = Nickname.first.user 114 | unless User === u 115 | t.error("belongs_to member was break") 116 | end 117 | unless u == User.first 118 | t.error("expect first user") 119 | end 120 | 121 | unless Nickname.last.user.name == "foo" 122 | t.error("belongs_to value broken") 123 | end 124 | 125 | begin 126 | Nickname.first.nothing 127 | rescue NameError => e 128 | unless e.message == "uninitialized constant Nickname::Nothing" 129 | t.error("Unexpected error message '#{e.message}'") 130 | end 131 | else 132 | t.error("expect raise NameError") 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/active_tsv/base_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_tsv' 2 | 3 | class User < ActiveTsv::Base 4 | self.table_path = "data/users.tsv" 5 | scope :thirty, -> { where(age: 30) } 6 | scope :age, ->(a) { where(age: a) } 7 | end 8 | 9 | class Nickname < ActiveTsv::Base 10 | self.table_path = "data/nicknames.tsv" 11 | end 12 | 13 | class Nokey < ActiveTsv::Base 14 | self.table_path = "data/nokeys.tsv" 15 | self.column_names = %w(foo bar baz) 16 | end 17 | 18 | module ActiveTsvBaseTest 19 | def test_s_encoding=(t) 20 | User.encoding = Encoding::ASCII_8BIT 21 | unless User.encoding == Encoding::ASCII_8BIT 22 | t.error("encoding couldn't change") 23 | end 24 | 25 | User.encoding = 'utf-8' 26 | unless User.encoding == Encoding::UTF_8 27 | t.error("encoding couldn't change") 28 | end 29 | 30 | begin 31 | User.encoding = 'nothing' 32 | rescue ArgumentError => e 33 | unless e.message == "unknown encoding name - nothing" 34 | t.error("Unexpected error") 35 | end 36 | else 37 | t.error("expect ArgumentError") 38 | end 39 | end 40 | 41 | def test_s_scope(t) 42 | unless User.thirty.to_a == User.where(age: 30).to_a 43 | t.error("named scope not expected behavior") 44 | end 45 | 46 | unless User.age(29) == User.where(age: 29) 47 | t.error("foo") 48 | end 49 | end 50 | 51 | def test_s_table_path(t) 52 | begin 53 | User.class_eval do 54 | self.table_path = nil 55 | end 56 | rescue TypeError 57 | else 58 | t.error("expect raise ArgumentError but nothing") 59 | end 60 | 61 | begin 62 | User.class_eval do 63 | self.table_path = 'a' 64 | end 65 | rescue Errno::ENOENT 66 | else 67 | t.error("expect raise ArgumentError but nothing") 68 | end 69 | 70 | User.class_eval do 71 | self.table_path = "data/names.tsv" 72 | end 73 | unless User.first.name == "foo" 74 | t.error("load error when table_path was changed") 75 | end 76 | 77 | ensure 78 | User.class_eval do 79 | self.table_path = "data/users.tsv" 80 | end 81 | end 82 | 83 | def test_s_all(t) 84 | r = User.all 85 | unless ActiveTsv::Relation === r 86 | t.error("break return value") 87 | end 88 | 89 | all = r.to_a 90 | unless all.all? { |i| User === i } 91 | t.error("unexpected classes") 92 | end 93 | 94 | unless all.length == 3 95 | t.error("unexpected size") 96 | end 97 | end 98 | 99 | def test_initialize(t) 100 | u = User.new 101 | unless User === u 102 | t.error("break return value") 103 | end 104 | 105 | { 106 | id: nil, 107 | name: nil, 108 | age: nil, 109 | }.each do |k, v| 110 | unless u[k] == v 111 | t.error("User##{k} expect #{v} got #{u[k]}") 112 | end 113 | end 114 | 115 | [ 116 | -> { User.new(1) }, 117 | -> { User.new(nil) }, 118 | ].each do |block| 119 | begin 120 | block.call 121 | rescue ArgumentError 122 | else 123 | t.error("Should raise ArgumentError but nothing") 124 | end 125 | end 126 | end 127 | 128 | def test_where(t) 129 | r = User.where(age: "30") 130 | unless ActiveTsv::Relation === r 131 | t.error("return value was break") 132 | end 133 | end 134 | 135 | def test_first(t) 136 | u = User.first 137 | unless User === u 138 | t.error("return value was break") 139 | end 140 | 141 | { 142 | id: "1", 143 | name: "ksss", 144 | age: "30", 145 | }.each do |k, v| 146 | unless u[k] == v 147 | t.error("User##{k} expect #{v} got #{u[k]}") 148 | end 149 | end 150 | end 151 | 152 | def test_last(t) 153 | u = User.last 154 | unless User === u 155 | t.error("break return value") 156 | end 157 | 158 | { 159 | id: "3", 160 | name: "bar", 161 | age: "30", 162 | }.each do |k, v| 163 | unless u[k] == v 164 | t.error("User##{k} expect #{v} got #{u[k]}") 165 | end 166 | end 167 | end 168 | 169 | def test_count(t) 170 | unless User.count === 3 171 | t.error("all count expect 3") 172 | end 173 | 174 | unless User.where(age: 30).count == 2 175 | t.error("where(age: 30) count expect 2") 176 | end 177 | end 178 | 179 | def test_initialize(t) 180 | u = User.new(:id => 10, "name" => 20) 181 | { id: 10, name: 20, age: nil }.each do |k, v| 182 | unless u[k] == v 183 | t.error("expect #{k}=#{v.inspect} but got #{k}=#{u[k].inspect}") 184 | end 185 | end 186 | 187 | begin 188 | User.new(foo: 10) 189 | rescue ActiveTsv::UnknownAttributeError 190 | else 191 | t.error("expect raise ActiveTsv::UnknownAttributeError bot was nothing") 192 | end 193 | end 194 | 195 | def test_equal(t) 196 | unless User.first == User.first 197 | t.error("expect same object") 198 | end 199 | unless User.first.eql?(User.first) 200 | t.error("expect same object") 201 | end 202 | unless !User.first.equal?(User.first) 203 | t.error("expect not equal object id") 204 | end 205 | end 206 | 207 | def test_column_names(t) 208 | [ 209 | [ User.column_names, %w(id name age) ], 210 | [ Nokey.column_names, %w(foo bar baz) ], 211 | ].each do |actual, expect| 212 | unless actual == expect 213 | t.error("expect #{expect.inspect} but got #{actual.inspect}") 214 | end 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /lib/active_tsv/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveTsv 4 | class Relation 5 | include Enumerable 6 | 7 | BUF_SIZE = 1024 8 | 9 | attr_reader :model 10 | attr_accessor :where_values 11 | attr_accessor :order_values 12 | attr_accessor :group_values 13 | 14 | def initialize(model) 15 | @model = model 16 | @where_values = [] 17 | @order_values = [] 18 | @group_values = [] 19 | end 20 | 21 | def initialize_copy(copy) 22 | copy.where_values = where_values.dup 23 | copy.order_values = order_values.dup 24 | copy.group_values = group_values.dup 25 | end 26 | 27 | def ==(other) 28 | where_values == other.where_values && 29 | order_values == other.order_values && 30 | group_values == other.group_values 31 | end 32 | 33 | def find(*ids) 34 | case ids.length 35 | when 0 36 | raise ActiveTsv::RecordNotFound, "Couldn't find #{@model} without an ID" 37 | when 1 38 | id = ids.first 39 | record = where(@model.primary_key => id).first 40 | unless record 41 | raise ActiveTsv::RecordNotFound, "Couldn't find #{@model} with '#{@model.primary_key}'=#{id}" 42 | end 43 | record 44 | else 45 | records = where(@model.primary_key => ids).to_a 46 | unless ids.length == records.length 47 | raise ActiveTsv::RecordNotFound, "Couldn't find all #{@model} with '#{@model.primary_key}': (#{ids.join(', ')}) (found #{records.length} results, but was looking for #{ids.length})" 48 | end 49 | records 50 | end 51 | end 52 | 53 | def where(where_value = nil) 54 | if where_value 55 | dup.tap do |r| 56 | r.where_values << Condition::Equal.new(where_value) 57 | end 58 | else 59 | WhereChain.new(dup) 60 | end 61 | end 62 | 63 | def pluck(*fields) 64 | key_to_value_index = @model.column_names.each_with_index.to_h 65 | if fields.empty? 66 | to_value_a 67 | elsif fields.one? 68 | to_value_a.map! { |v| v[key_to_value_index[fields.first.to_s]] } 69 | else 70 | indexes = fields.map(&:to_s).map! { |field| key_to_value_index[field] } 71 | to_value_a.map! { |v| v.values_at(*indexes) } 72 | end 73 | end 74 | 75 | def exists? 76 | !first.nil? 77 | end 78 | 79 | def first 80 | if @order_values.empty? 81 | each_model.first 82 | else 83 | to_a.first 84 | end 85 | end 86 | 87 | def last 88 | if @where_values.empty? && @order_values.empty? 89 | last_value = File.open(@model.table_path, "r:#{@model.encoding}:UTF-8") do |f| 90 | f.seek(0, IO::SEEK_END) 91 | buf_size = [f.size, self.class::BUF_SIZE].min 92 | while true 93 | f.seek(-buf_size, IO::SEEK_CUR) 94 | buf = f.read(buf_size) 95 | if index = buf.rindex($INPUT_RECORD_SEPARATOR, -2) 96 | f.seek(-buf_size + index + 1, IO::SEEK_CUR) 97 | break f.read.chomp 98 | else 99 | f.seek(-buf_size, IO::SEEK_CUR) 100 | end 101 | end 102 | end 103 | @model.new(CSV.new(last_value, col_sep: @model::SEPARATER).shift) 104 | else 105 | @model.new(to_value_a.last) 106 | end 107 | end 108 | 109 | def take(n = nil) 110 | if n 111 | if @order_values.empty? 112 | each_model.take(n) 113 | else 114 | to_value_a.take(n).map! { |i| @model.new(i) } 115 | end 116 | else 117 | first 118 | end 119 | end 120 | 121 | def count 122 | if @group_values.empty? 123 | super 124 | else 125 | h = if @group_values.one? 126 | group_by { |i| i[@group_values.first] } 127 | else 128 | group_by { |i| @group_values.map { |c| i[c] } } 129 | end 130 | h.each do |k, v| 131 | h[k] = v.count 132 | end 133 | h 134 | end 135 | end 136 | 137 | def order(*columns) 138 | @order_values += order_conditions(columns) 139 | @order_values.uniq! 140 | self 141 | end 142 | 143 | def group(*columns) 144 | @group_values += columns 145 | @group_values.uniq! 146 | self 147 | end 148 | 149 | def each(*args, &block) 150 | to_a.each(*args, &block) 151 | end 152 | 153 | def to_a 154 | to_value_a.map! { |v| @model.new(v) } 155 | end 156 | 157 | def inspect 158 | a = to_value_a.take(11).map! { |i| @model.new(i) }.map!(&:inspect) 159 | a[10] = '...' if a.length == 11 160 | 161 | "#<#{self.class.name} [#{a.join(', ')}]>" 162 | end 163 | 164 | def maximum(column) 165 | pluck(column).max 166 | end 167 | 168 | def minimum(column) 169 | pluck(column).min 170 | end 171 | 172 | private 173 | 174 | def to_value_a 175 | ret = each_value.to_a 176 | if @order_values.empty?.! 177 | key_to_value_index = @model.column_names.each_with_index.to_h 178 | if @order_values.one? 179 | order_condition = @order_values.first 180 | index = key_to_value_index[order_condition.column] 181 | ret.sort_by! { |i| i[index] } 182 | ret.reverse! if order_condition.descending? 183 | else 184 | ret.sort! do |a, b| 185 | @order_values.each.with_index(1) do |order_condition, index| 186 | comp = a[key_to_value_index[order_condition.column]] <=> b[key_to_value_index[order_condition.column]] 187 | break 0 if comp == 0 && index == @order_values.length 188 | break comp * order_condition.to_i if comp != 0 189 | end 190 | end 191 | end 192 | end 193 | ret 194 | end 195 | 196 | def each_value 197 | return to_enum(__method__) unless block_given? 198 | 199 | key_to_value_index = @model.column_names.each_with_index.to_h 200 | @model.open do |csv| 201 | csv.gets if !@model.custom_column_name? 202 | csv.each do |value| 203 | yield value if @where_values.all? do |cond| 204 | case cond 205 | when Condition::Equal 206 | cond.values.all? do |k, v| 207 | index = key_to_value_index[k.to_s] 208 | raise StatementInvalid, "no such column: #{k}" unless index 209 | if v.respond_to?(:to_a) 210 | v.to_a.any? { |vv| value[index] == vv.to_s } 211 | else 212 | value[index] == v.to_s 213 | end 214 | end 215 | when Condition::NotEqual 216 | cond.values.all? do |k, v| 217 | index = key_to_value_index[k.to_s] 218 | raise StatementInvalid, "no such column: #{k}" unless index 219 | if v.respond_to?(:to_a) 220 | !v.to_a.any? { |vv| value[index] == vv.to_s } 221 | else 222 | !(value[index] == v.to_s) 223 | end 224 | end 225 | end 226 | end 227 | end 228 | end 229 | end 230 | 231 | def each_model 232 | return to_enum(__method__) unless block_given? 233 | 234 | each_value { |v| yield @model.new(v) } 235 | end 236 | 237 | def order_conditions(columns) 238 | columns.map { |column| 239 | case column 240 | when Symbol, String 241 | Ordering::Ascending.new(column.to_s) 242 | when Hash 243 | column.map do |col, direction| 244 | case direction 245 | when :asc, :ASC, "asc", "ASC" 246 | Ordering::Ascending.new(col.to_s) 247 | when :desc, :DESC, "desc", "DESC" 248 | Ordering::Descending.new(col.to_s) 249 | else 250 | raise ArgumentError, %(Direction "#{direction}" is invalid. Valid directions are: #{Ordering::VALID_DIRECTIONS}) 251 | end 252 | end 253 | else 254 | raise TypeError, "Cannot visit #{column.class}" 255 | end 256 | }.flatten 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/active_tsv/relation_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_tsv' 2 | 3 | module ActiveTsvRelationTest 4 | class User < ActiveTsv::Base 5 | self.table_path = "data/users.tsv" 6 | end 7 | 8 | class Nokey < ActiveTsv::Base 9 | self.table_path = "data/nokeys.tsv" 10 | self.column_names = %w(foo bar baz) 11 | end 12 | 13 | def test_find(t) 14 | unless User.find(1).name == "ksss" 15 | t.error("Couldn't find 'id'=1") 16 | end 17 | 18 | unless User.where(age: 30).find(1).name == "ksss" 19 | t.error("Couldn't find 'id'=1") 20 | end 21 | 22 | unless User.find(1, 2).map(&:name) == ["ksss", "foo"] 23 | t.error("Couldn't find 'id'=1 and 2") 24 | end 25 | 26 | c = Struct.new(:code, :expect) 27 | [ 28 | c.new(-> { User.find }, "Couldn't find ActiveTsvRelationTest::User without an ID"), 29 | c.new(-> { User.where(age: 300).find(1) }, "Couldn't find ActiveTsvRelationTest::User with 'id'=1"), 30 | c.new(-> { User.find(100) }, "Couldn't find ActiveTsvRelationTest::User with 'id'=100"), 31 | c.new(-> { User.find(1, 100) }, "Couldn't find all ActiveTsvRelationTest::User with 'id': (1, 100) (found 1 results, but was looking for 2)"), 32 | ].each do |set| 33 | begin 34 | set.code.call 35 | rescue ActiveTsv::RecordNotFound => e 36 | unless e.message == set.expect 37 | t.error("Unexpected error message") 38 | end 39 | else 40 | t.error("expect ActiveTsv::RecordNotFound") 41 | end 42 | end 43 | end 44 | 45 | def test_where(t) 46 | r = User.where(age: 30).where(name: "ksss") 47 | unless ActiveTsv::Relation === r 48 | t.error("break return value #{r}") 49 | end 50 | a = r.to_a 51 | unless a.length == 1 52 | t.error("expect length 1 got #{a.length}") 53 | end 54 | 55 | r = Nokey.where(foo: 2) 56 | expect = [Nokey.new(foo: "2", bar: "3", baz: "4")] 57 | unless r.to_a == expect 58 | t.error("expect #{expect} but got #{r.to_a}") 59 | end 60 | end 61 | 62 | def test_where_in(t) 63 | cond = ["1", "2"] 64 | unless User.where(id: cond).map(&:name) == ["ksss", "foo"] 65 | t.error("break where id in #{cond}") 66 | end 67 | 68 | unless User.where.not(id: cond).map(&:name) == ["bar"] 69 | t.error("break where id not in #{cond}") 70 | end 71 | 72 | unless User.where(id: 1..2).map(&:name) == ["ksss", "foo"] 73 | t.error("break where id in #{cond}") 74 | end 75 | end 76 | 77 | def test_where_unknown_column(t) 78 | [ 79 | User.where(unknown: 1), 80 | User.where.not(unknown: 1), 81 | ].each do |r| 82 | begin 83 | r.to_a 84 | rescue ActiveTsv::StatementInvalid => e 85 | unless e.message == "no such column: unknown" 86 | t.error("unexpected error message '#{e.message}'") 87 | end 88 | else 89 | t.error("expect raise ActiveTsv::StatementInvalid") 90 | end 91 | end 92 | end 93 | 94 | def test_equal(t) 95 | unless User.all == User.all 96 | t.error("expect same") 97 | end 98 | unless User.where.not(age: 30).where(name: 'ksss') == User.where.not(age: 30).where(name: 'ksss') 99 | t.error("expect same") 100 | end 101 | end 102 | 103 | def test_where_scope(t) 104 | r = User.all 105 | r1 = r.where(age: 30) 106 | r2 = r.where(name: 'ksss') 107 | unless r1.to_a.length == 2 108 | t.error("expect keep scope but dose not") 109 | end 110 | 111 | unless r2.to_a.length == 1 112 | t.error("expect keep scope but dose not") 113 | end 114 | end 115 | 116 | def test_each(t) 117 | r = User.where(age: 30) 118 | r.each do |u| 119 | unless User === u 120 | t.error("break iterate item") 121 | end 122 | end 123 | 124 | unless Enumerator === r.each 125 | t.error("break return value") 126 | end 127 | end 128 | 129 | def test_to_a(t) 130 | a = User.where(age: "30").to_a 131 | unless Array === a 132 | t.error("break return value #{a}") 133 | end 134 | unless a.length == 2 135 | t.error("expect length 2 got #{a.length}") 136 | end 137 | end 138 | 139 | def test_pluck(t) 140 | unless User.pluck == User.all.to_a.map { |i| [i.id, i.name, i.age] } 141 | t.error("break values") 142 | end 143 | 144 | unless User.pluck(:id) == User.all.to_a.map(&:id) 145 | t.error("break values") 146 | end 147 | 148 | unless User.order(:name).pluck(:id) == User.order(:name).to_a.map(&:id) 149 | t.error("break values") 150 | end 151 | 152 | unless User.pluck(:id, :name) == User.all.to_a.map { |i| [i.id, i.name] } 153 | t.error("break values") 154 | end 155 | end 156 | 157 | def test_order(t) 158 | begin 159 | User.order(Object.new) 160 | rescue TypeError 161 | else 162 | t.error("expect raise TypeError but nothing") 163 | end 164 | 165 | r = User.order(:age, :name) 166 | unless ActiveTsv::Relation === r 167 | t.error("break return value") 168 | end 169 | 170 | unless r.to_a.map(&:name) == ["foo", "bar", "ksss"] 171 | t.error("miss order") 172 | end 173 | 174 | unless User.order(:name).where.not(age: 29).map(&:name) == User.where.not(age: 29).order(:name).map(&:name) 175 | t.error("expect match order where.order and order.where") 176 | t.log(User.order(:name).where.not(age: 29).map(&:name), User.where.not(age: 29).order(:name).map(&:name)) 177 | end 178 | end 179 | 180 | def test_order_desc(t) 181 | unless User.order(name: :desc).map(&:name) == User.order(:name).map(&:name).reverse 182 | t.error("order descending is not equal reverse it") 183 | end 184 | 185 | unless User.order(age: :asc).order(name: :desc).map(&:name) == ["foo", "ksss", "bar"] 186 | t.error("ordering was break") 187 | end 188 | 189 | begin 190 | User.order(age: :typo) 191 | rescue ArgumentError 192 | else 193 | t.error("expect raise ArgumentError") 194 | end 195 | end 196 | 197 | def test_ordered_first_and_last(t) 198 | unless User.order(:name).first.name == "bar" 199 | t.error("first record didn't change by order") 200 | end 201 | unless User.order(:name).last.name == "ksss" 202 | t.error("last record didn't change by order") 203 | end 204 | end 205 | 206 | def test_order_reorderable(t) 207 | r = User.order(:name).where.not(age: 29) 208 | first_last = nil 209 | r.each { |i| first_last = i } 210 | second_last = nil 211 | r.each { |i| second_last = i } 212 | unless first_last.name == second_last.name 213 | t.error("break order_values") 214 | end 215 | end 216 | 217 | def test_take(t) 218 | r = User.all 219 | unless r.take(2).length == 2 220 | t.error("take(2) expect get length 2") 221 | end 222 | 223 | unless r.take.name == "ksss" 224 | t.error("take expect like first") 225 | end 226 | end 227 | 228 | def test_take_with_order(t) 229 | unless User.order(:name).take(2).map(&:name) == ["bar", "foo"] 230 | t.error("`take` should consider order") 231 | end 232 | 233 | unless User.order(:name).take.name == "bar" 234 | t.error("`take` should consider order") 235 | end 236 | end 237 | 238 | def test_group(t) 239 | r = User.all 240 | unless r.count == 3 241 | t.error("expect count 3 got #{r.count}") 242 | end 243 | 244 | one = r.group(:age).count 245 | expect = { "30" => 2, "29" => 1 } 246 | unless one == expect 247 | t.error("expect #{expect} got #{one}") 248 | end 249 | 250 | two = r.group(:age, :name).count 251 | expect = { ["30", "ksss"] => 1, ["29", "foo"] => 1, ["30", "bar"] => 1 } 252 | unless two == expect 253 | t.error("expect #{expect} got #{two}") 254 | end 255 | end 256 | 257 | def test_maximum(t) 258 | unless User.maximum(:name) == "ksss" 259 | t.error("Cannot get maximum value") 260 | end 261 | 262 | unless User.where.not(name: "ksss").maximum(:name) == "foo" 263 | t.error("Cannot get maximum value") 264 | end 265 | 266 | unless User.where.not(id: "1").where.not(id: "2").where.not(id: "3").maximum(:id).nil? 267 | t.error("expect nil") 268 | end 269 | end 270 | 271 | def test_minimum(t) 272 | unless User.minimum(:name) == "bar" 273 | t.error("Cannot get maximum value") 274 | end 275 | 276 | unless User.where.not(name: "bar").minimum(:name) == "foo" 277 | t.error("Cannot get maximum value") 278 | end 279 | 280 | unless User.where.not(id: "1").where.not(id: "2").where.not(id: "3").minimum(:id).nil? 281 | t.error("expect nil") 282 | end 283 | end 284 | end 285 | --------------------------------------------------------------------------------