volt-sql 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +10 -0
- data/README.md +31 -0
- data/Rakefile +1 -0
- data/app/sql/config/dependencies.rb +1 -0
- data/app/sql/config/initializers/boot.rb +5 -0
- data/app/sql/config/routes.rb +1 -0
- data/app/sql/controllers/main_controller.rb +5 -0
- data/app/sql/lib/field_updater.rb +103 -0
- data/app/sql/lib/helper.rb +121 -0
- data/app/sql/lib/index_updater.rb +82 -0
- data/app/sql/lib/migration.rb +52 -0
- data/app/sql/lib/migration_generator.rb +44 -0
- data/app/sql/lib/reconcile.rb +51 -0
- data/app/sql/lib/sql_adaptor_client.rb +110 -0
- data/app/sql/lib/sql_adaptor_server.rb +438 -0
- data/app/sql/lib/sql_logger.rb +13 -0
- data/app/sql/lib/store_persistor.rb +10 -0
- data/app/sql/lib/table_reconcile.rb +123 -0
- data/app/sql/lib/where_call.rb +64 -0
- data/app/sql/views/main/index.html +2 -0
- data/config/db/development.db +0 -0
- data/config/db/test.db +0 -0
- data/lib/volt/sql/version.rb +5 -0
- data/lib/volt/sql.rb +27 -0
- data/spec/postgres/lib/helpers.rb +22 -0
- data/spec/postgres/lib/index_updater_spec.rb +88 -0
- data/spec/postgres/lib/postgres_where_call_spec.rb +59 -0
- data/spec/postgres/lib/table_reconcile_spec.rb +178 -0
- data/spec/spec_helper.rb +36 -0
- data/volt-sql.gemspec +23 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 918bb2e8b15738d1e2d2b80cba4c09cfa24f26a8
|
4
|
+
data.tar.gz: a52245cae20149dec9fa0033c92ca382ef800eb9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1383480587bfd8b8eed8ebcde8c56ebd68163366622fe9fb63c7d61afa52cea5bad104c6f0ff217bcaaecbd7782f7b5dbcdc77b9b10af66f46f981df6b1d182d
|
7
|
+
data.tar.gz: e52a71cfb0f1a3941fbd67f9ae62ba01e934b90a27f98440c57c27655dbdb95212af6f99764254f4d776aac4e11cbfb88a59bb1863939becb6e81f85e46b32c8
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# volt-sql
|
2
|
+
|
3
|
+
**Note**: This is still a work in process. We'll update this when the final version is out.
|
4
|
+
|
5
|
+
This gem provides postgres support for volt. Just include it in your gemfile and the gem will do the rest. It is included in new projects by default (currently).
|
6
|
+
|
7
|
+
```
|
8
|
+
gem 'volt-sql'
|
9
|
+
gem 'pg', '~> 0.18.2'
|
10
|
+
gem 'pg_json', '~> 0.1.29'
|
11
|
+
'
|
12
|
+
```
|
13
|
+
|
14
|
+
|
15
|
+
## Big Thanks
|
16
|
+
|
17
|
+
First off, I wanted to say a big thanks to @jeremyevans for his hard work on Sequel, without which, this gem would not be possible.
|
18
|
+
|
19
|
+
## How migrations work:
|
20
|
+
|
21
|
+
When an app boots in dev mode, or a file is changed in dev mode, and a new field is detected, the following happens:
|
22
|
+
|
23
|
+
if there are no orphan fields (fields that exist in the db, but not in the Models class):
|
24
|
+
- the field is added
|
25
|
+
|
26
|
+
- If there is a single orphaned field and a single added field in the model:
|
27
|
+
- a migration is generated to rename the field
|
28
|
+
- the migration is run
|
29
|
+
|
30
|
+
- If there are multiple orphans or fields added
|
31
|
+
- volt warns you and makes you deal with it (by creating migrations)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1 @@
|
|
1
|
+
# Component dependencies
|
@@ -0,0 +1 @@
|
|
1
|
+
# Component routes
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# The FieldUpdater class is responsible for adding or updating a field, either
|
2
|
+
# by calling sequel directly, or by generating a migration, then running it.
|
3
|
+
require 'sql/lib/sql_logger'
|
4
|
+
|
5
|
+
module Volt
|
6
|
+
module Sql
|
7
|
+
class FieldUpdater
|
8
|
+
include SqlLogger
|
9
|
+
|
10
|
+
def initialize(db, table_reconcile)
|
11
|
+
@db = db
|
12
|
+
@table_reconcile = table_reconcile
|
13
|
+
end
|
14
|
+
|
15
|
+
# Update a field (or create it)
|
16
|
+
def update_field(model_class, table_name, db_field, column_name, klasses, opts)
|
17
|
+
sequel_class, sequel_opts = Helper.column_type_and_options_for_sequel(klasses, opts)
|
18
|
+
|
19
|
+
# Check if column exists
|
20
|
+
if !db_field
|
21
|
+
|
22
|
+
log("Add field #{column_name} to #{table_name}")
|
23
|
+
# Column does not exist, add it.
|
24
|
+
# Make sure klass matches
|
25
|
+
@db.add_column(table_name, column_name, sequel_class, sequel_opts)
|
26
|
+
else
|
27
|
+
db_class, db_opts = @table_reconcile.sequel_class_and_opts_from_db(db_field)
|
28
|
+
|
29
|
+
if db_class != sequel_class || db_opts != sequel_opts
|
30
|
+
|
31
|
+
# Data type has changed, migrate
|
32
|
+
up_code = []
|
33
|
+
down_code = []
|
34
|
+
|
35
|
+
if db_opts != sequel_opts
|
36
|
+
# Fetch allow_null, keeping in mind it defaults to true
|
37
|
+
db_null = db_opts.fetch(:allow_null, true)
|
38
|
+
sequel_null = sequel_opts.fetch(:allow_null, true)
|
39
|
+
|
40
|
+
if db_null != sequel_null
|
41
|
+
# allow null changed
|
42
|
+
if sequel_null
|
43
|
+
up_code << "set_column_allow_null #{table_name.inspect}, #{column_name.inspect}"
|
44
|
+
down_code << "set_column_not_null #{table_name.inspect}, #{column_name.inspect}"
|
45
|
+
else
|
46
|
+
up_code << "set_column_not_null #{table_name.inspect}, #{column_name.inspect}"
|
47
|
+
down_code << "set_column_allow_null #{table_name.inspect}, #{column_name.inspect}"
|
48
|
+
end
|
49
|
+
|
50
|
+
db_opts.delete(:allow_null)
|
51
|
+
sequel_opts.delete(:allow_null)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
if db_class != sequel_class || db_opts != sequel_opts
|
57
|
+
up_code << "set_column_type #{table_name.inspect}, #{column_name.inspect}, #{sequel_class}, #{sequel_opts.inspect}"
|
58
|
+
down_code << "set_column_type #{table_name.inspect}, #{column_name.inspect}, #{db_class}, #{db_opts.inspect}"
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
if up_code.present?
|
63
|
+
generate_and_run("column_change_#{table_name.to_s.gsub('/', '_')}_#{column_name}", up_code.join("\n"), down_code.join("\n"))
|
64
|
+
end
|
65
|
+
|
66
|
+
# TODO: Improve message
|
67
|
+
# raise "Data type changed, can not migrate field #{name} from #{db_field.inspect} to #{klass.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def auto_migrate_field_rename(table_name, from_name, to_name)
|
75
|
+
log("Rename #{from_name} to #{to_name} on table #{table_name}")
|
76
|
+
|
77
|
+
name = "rename_#{table_name}_#{from_name}_to_#{to_name}"
|
78
|
+
up_code = "rename_column #{table_name.inspect}, #{from_name.inspect}, #{to_name.inspect}"
|
79
|
+
down_code = "rename_column #{table_name.inspect}, #{to_name.inspect}, #{from_name.inspect}"
|
80
|
+
generate_and_run(name, up_code, down_code)
|
81
|
+
end
|
82
|
+
|
83
|
+
def auto_migrate_remove_field(table_name, column_name, db_field)
|
84
|
+
log("Remove #{column_name} from table #{table_name}")
|
85
|
+
|
86
|
+
name = "remove_#{table_name}_#{column_name}"
|
87
|
+
up_code = "drop_column #{table_name.inspect}, #{column_name.inspect}"
|
88
|
+
|
89
|
+
sequel_class, sequel_options = @table_reconcile.sequel_class_and_opts_from_db(db_field)
|
90
|
+
down_code = "add_column #{table_name.inspect}, #{column_name.inspect}, #{sequel_class}, #{sequel_options.inspect}"
|
91
|
+
generate_and_run(name, up_code, down_code)
|
92
|
+
end
|
93
|
+
|
94
|
+
# private
|
95
|
+
def generate_and_run(name, up_code, down_code)
|
96
|
+
path = Volt::Sql::MigrationGenerator.create_migration(name, up_code, down_code)
|
97
|
+
|
98
|
+
Volt::MigrationRunner.new(@db).run_migration(path, :up)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# A few pure functions for converting between volt types/options and sequel
|
2
|
+
# type/options
|
3
|
+
|
4
|
+
module Volt
|
5
|
+
module Sql
|
6
|
+
module Helper
|
7
|
+
|
8
|
+
|
9
|
+
# This method takes in info from the db schema and returns the volt field
|
10
|
+
# klass and options that would have been used to create it.
|
11
|
+
#
|
12
|
+
# @returns [Array of klasses, Hash of options]
|
13
|
+
def self.klasses_and_options_from_db(db_field)
|
14
|
+
klasses = []
|
15
|
+
options = {}
|
16
|
+
|
17
|
+
# merge values based on map (options key from db_key)
|
18
|
+
{
|
19
|
+
:text => :text,
|
20
|
+
:size => :max_length,
|
21
|
+
:nil => :allow_null,
|
22
|
+
:default => :ruby_default
|
23
|
+
}.each_pair do |opts_key, db_key|
|
24
|
+
options[opts_key] = db_field[db_key] if db_field.has_key?(db_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
options.delete(:default) if options[:default] == nil
|
28
|
+
|
29
|
+
db_type = db_field[:db_type].to_sym
|
30
|
+
|
31
|
+
case db_field[:type]
|
32
|
+
when :string
|
33
|
+
klasses << String
|
34
|
+
when :datetime
|
35
|
+
klasses << VoltTime
|
36
|
+
when :boolean
|
37
|
+
klasses << Volt::Boolean
|
38
|
+
when :float
|
39
|
+
klasses << Float
|
40
|
+
else
|
41
|
+
case db_type
|
42
|
+
when :text
|
43
|
+
when :string
|
44
|
+
klasses << String
|
45
|
+
when :numeric
|
46
|
+
klasses << Numeric
|
47
|
+
when :integer
|
48
|
+
klasses << Fixnum
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if klasses.size == 0
|
53
|
+
raise "Could not match database type #{db_type} in #{db_field.inspect}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Default is to allow nil
|
57
|
+
unless options[:nil] == false
|
58
|
+
klasses << NilClass
|
59
|
+
end
|
60
|
+
options.delete(:nil)
|
61
|
+
|
62
|
+
return klasses, options
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Takes in the klass and options specified on the model or an add_column
|
67
|
+
# and returns the correct klass/options for add_column in sequel.
|
68
|
+
def self.column_type_and_options_for_sequel(klasses, options)
|
69
|
+
options = options.dup
|
70
|
+
|
71
|
+
# Remove from start fields
|
72
|
+
klasses ||= [String, NilClass]
|
73
|
+
|
74
|
+
allow_nil = klasses.include?(NilClass)
|
75
|
+
klasses = klasses.reject {|klass| klass == NilClass }
|
76
|
+
|
77
|
+
if klasses.size > 1
|
78
|
+
raise MultiTypeException, 'the sql adaptor only supports a single type (or NilClass) for each field.'
|
79
|
+
end
|
80
|
+
|
81
|
+
klass = klasses.first
|
82
|
+
|
83
|
+
if options.has_key?(:nil)
|
84
|
+
options[:allow_null] = options.delete(:nil)
|
85
|
+
else
|
86
|
+
options[:allow_null] = allow_nil
|
87
|
+
end
|
88
|
+
|
89
|
+
if klass == String
|
90
|
+
# If no length restriction, make it text
|
91
|
+
if options[:size]
|
92
|
+
if options[:size] > 255
|
93
|
+
# Make the field text
|
94
|
+
options.delete(:size)
|
95
|
+
options[:text] = true
|
96
|
+
end
|
97
|
+
else
|
98
|
+
options[:text] = true
|
99
|
+
end
|
100
|
+
elsif klass == VoltTime
|
101
|
+
klass = Time
|
102
|
+
elsif klass == Volt::Boolean || klass == TrueClass || klass == FalseClass
|
103
|
+
klass = TrueClass # what sequel uses for booleans
|
104
|
+
end
|
105
|
+
|
106
|
+
return klass, options
|
107
|
+
end
|
108
|
+
|
109
|
+
# When asking for indexes on a table, the deferrable option will show
|
110
|
+
# as nil if it wasn't set, we remove this to normalize it to the volt
|
111
|
+
# options.
|
112
|
+
def self.normalized_indexes_from_table(db, table_name)
|
113
|
+
db.indexes(table_name).map do |name, options|
|
114
|
+
# Remove the deferrable that defaults to false (nil)
|
115
|
+
options = options.reject {|key, value| key == :deferrable && !value }
|
116
|
+
[name, options]
|
117
|
+
end.to_h
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# The IndexUpdater looks at the current indexes on a table and those in the db
|
2
|
+
# and reconciles them. Since indexes are immutable, this means dropping any
|
3
|
+
# that don't match and creating new ones. (simple) This also means we don't
|
4
|
+
# need any migrations.
|
5
|
+
|
6
|
+
require 'sql/lib/sql_logger'
|
7
|
+
require 'sql/lib/helper'
|
8
|
+
|
9
|
+
module Volt
|
10
|
+
module Sql
|
11
|
+
class IndexUpdater
|
12
|
+
include SqlLogger
|
13
|
+
|
14
|
+
def initialize(db, model_class, table_name)
|
15
|
+
@db = db
|
16
|
+
@table_name = table_name
|
17
|
+
|
18
|
+
model_indexes = model_class.indexes
|
19
|
+
db_indexes = Helper.normalized_indexes_from_table(@db, table_name)
|
20
|
+
|
21
|
+
model_indexes.each_pair do |name, options|
|
22
|
+
# See if we have a matching columns/options
|
23
|
+
if db_indexes[name] == options
|
24
|
+
# Matches, ignore it
|
25
|
+
db_indexes.delete(name)
|
26
|
+
else
|
27
|
+
# Something changed, if a db_index for the name exists,
|
28
|
+
# delete it, because the options changed
|
29
|
+
if (db_opts = db_indexes[name])
|
30
|
+
# Drop the index, drop it from the liast of db_indexes
|
31
|
+
drop_index(name, db_opts)
|
32
|
+
db_indexes.delete(name)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create the new index
|
36
|
+
add_index(name, options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# drop any remaining db_indexes, because they are no longer defined in
|
41
|
+
# the model
|
42
|
+
db_indexes.each do |name, options|
|
43
|
+
drop_index(name, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
@db.indexes(table_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def drop_index(name, options)
|
51
|
+
columns, options = columns_and_options(name, options)
|
52
|
+
log("drop index on #{columns.inspect}, #{options.inspect}")
|
53
|
+
|
54
|
+
@db.alter_table(@table_name) do
|
55
|
+
drop_index(columns, options.select {|k,v| k == :name })
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_index(name, options)
|
60
|
+
columns, options = columns_and_options(name, options)
|
61
|
+
log("add index on #{columns.inspect}, #{options.inspect}")
|
62
|
+
@db.alter_table(@table_name) do
|
63
|
+
add_index(columns, options)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Convert to the columns, options(with name) format used by sequel for
|
70
|
+
# add_index/drop_index
|
71
|
+
def columns_and_options(name, options)
|
72
|
+
options = options.dup
|
73
|
+
columns = options.delete(:columns)
|
74
|
+
|
75
|
+
options[:name] = name
|
76
|
+
|
77
|
+
return columns, options
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Add database methods for the migration class
|
2
|
+
module Volt
|
3
|
+
class Migration
|
4
|
+
def initialize(db=nil)
|
5
|
+
@db = db || Volt.current_app.database.db
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_column(table_name, column_name, klasses, options={})
|
9
|
+
sequel_type, sequel_options = Helper.column_type_and_options_for_sequel(klasses, options)
|
10
|
+
@db.alter_table(table_name) do
|
11
|
+
add_column(column_name, sequel_type, sequel_options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def rename_column(table_name, from, to, options={})
|
16
|
+
# TODO: add options check
|
17
|
+
@db.alter_table(table_name) do
|
18
|
+
rename_column from, to
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def drop_column(table_name, column_name)
|
23
|
+
@db.alter_table(table_name) do
|
24
|
+
drop_column column_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_column_type(table_name, column_name, type, options={})
|
29
|
+
@db.alter_table(table_name) do
|
30
|
+
set_column_type(column_name, type, options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_column_allow_null(table_name, column_name)
|
35
|
+
@db.alter_table(table_name) do
|
36
|
+
set_column_allow_null(column_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_column_not_null(table_name, column_name)
|
41
|
+
@db.alter_table(table_name) do
|
42
|
+
set_column_not_null(column_name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_column_default(table_name, column_name, default)
|
47
|
+
@db.alter_table(table_name) do
|
48
|
+
set_column_default(column_name, default)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Run and create migrations programatically.
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Volt
|
5
|
+
module Sql
|
6
|
+
module MigrationGenerator
|
7
|
+
def self.create_migration(name, up_content, down_content)
|
8
|
+
timestamp = Time.now.to_i
|
9
|
+
file_name = "#{timestamp}_#{name.underscore}"
|
10
|
+
class_name = name.camelize
|
11
|
+
output_file = "#{Dir.pwd}/config/db/migrations/#{file_name}.rb"
|
12
|
+
|
13
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
14
|
+
|
15
|
+
content = <<-END.gsub(/^ {8}/, '')
|
16
|
+
class #{class_name} < Volt::Migration
|
17
|
+
def up
|
18
|
+
#{indent_string(up_content, 4)}
|
19
|
+
end
|
20
|
+
|
21
|
+
def down
|
22
|
+
#{indent_string(down_content, 4)}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
END
|
26
|
+
|
27
|
+
File.open(output_file, 'w') {|f| f.write(content) }
|
28
|
+
|
29
|
+
# return the path to the file
|
30
|
+
output_file
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.indent_string(string, count)
|
34
|
+
string.split("\n").map.with_index do |line,index|
|
35
|
+
if index == 0
|
36
|
+
string
|
37
|
+
else
|
38
|
+
(' ' * count) + string
|
39
|
+
end
|
40
|
+
end.join("\n")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'sql/lib/table_reconcile'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Volt
|
5
|
+
# Add reconcile! directly to the model (on the server)
|
6
|
+
class Model
|
7
|
+
class_attribute :reconciled
|
8
|
+
end
|
9
|
+
|
10
|
+
class MultiTypeException < Exception
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
module Sql
|
15
|
+
class Reconcile
|
16
|
+
attr_reader :db
|
17
|
+
def initialize(adaptor, db)
|
18
|
+
@adaptor = adaptor
|
19
|
+
@db = db
|
20
|
+
end
|
21
|
+
|
22
|
+
# reconcile takes the database from its current state to the state defined
|
23
|
+
# in the model classes with the field helper
|
24
|
+
def reconcile!
|
25
|
+
Volt::RootModels.model_classes.each do |model_class|
|
26
|
+
TableReconcile.new(@adaptor, @db, model_class).run
|
27
|
+
end
|
28
|
+
|
29
|
+
# After the initial reconcile!, we add a listener for any new models
|
30
|
+
# created, so we can reconcile them (in specs mostly)
|
31
|
+
reset!
|
32
|
+
@@listener = RootModels.on('model_created') do |model_class|
|
33
|
+
# We do a full invalidate and wait for the next db access, because the
|
34
|
+
# model_created gets called before the class is actually fully defined.
|
35
|
+
# (ruby inherited limitation)
|
36
|
+
@adaptor.invalidate_reconcile!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# Called to clear the listener
|
42
|
+
def reset!
|
43
|
+
if defined?(@@listener) && @@listener
|
44
|
+
@@listener.remove
|
45
|
+
@@listener = nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Volt
|
2
|
+
class DataStore
|
3
|
+
class SqlAdaptorClient < BaseAdaptorClient
|
4
|
+
data_store_methods :where, :offset, :skip, :order, :limit, :count, :includes
|
5
|
+
|
6
|
+
module SqlArrayStore
|
7
|
+
def skip(*args)
|
8
|
+
add_query_part(:offset, *args)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Count without arguments or a block makes its own query to the backend.
|
12
|
+
# If you pass an arg or block, it will run ```all``` on the Cursor, then
|
13
|
+
# run a normal ruby ```.count``` on it, passing the args.
|
14
|
+
def count(*args, &block)
|
15
|
+
if args || block
|
16
|
+
@model.reactive_count(*args, &block)
|
17
|
+
else
|
18
|
+
cursor = add_query_part(:count)
|
19
|
+
|
20
|
+
cursor.persistor.value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Due to the way define_method works, we need to remove the generated
|
26
|
+
# methods from data_store_methods before we over-ride them.
|
27
|
+
Volt::Persistors::ArrayStore.send(:remove_method, :skip)
|
28
|
+
Volt::Persistors::ArrayStore.send(:remove_method, :count)
|
29
|
+
|
30
|
+
# include sql's methods on ArrayStore
|
31
|
+
Volt::Persistors::ArrayStore.send(:include, SqlArrayStore)
|
32
|
+
|
33
|
+
module SqlArrayModel
|
34
|
+
def dataset
|
35
|
+
Volt::DataStore.fetch(Volt.current_app).db.from(collection_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Volt::ArrayModel.send(:include, SqlArrayModel)
|
40
|
+
|
41
|
+
|
42
|
+
# In the volt query dsl (and sql), there's a lot of ways to express the
|
43
|
+
# same query. Its better for performance however if queries can be
|
44
|
+
# uniquely identified. To make that happen, we normalize queries.
|
45
|
+
def self.normalize_query(query)
|
46
|
+
# query = convert_wheres_to_block(query)
|
47
|
+
query = merge_wheres_and_move_to_front(query)
|
48
|
+
|
49
|
+
query = reject_offset_zero(query)
|
50
|
+
|
51
|
+
query
|
52
|
+
end
|
53
|
+
|
54
|
+
# Where's can use either a hash arg, or a block. If the where has a hash
|
55
|
+
# arg, we convert it to block style, so it can be unified.
|
56
|
+
def self.convert_wheres_to_block(query)
|
57
|
+
wheres = []
|
58
|
+
|
59
|
+
query.reject! do |query_part|
|
60
|
+
if query_part[0] == 'where'
|
61
|
+
wheres << query_part
|
62
|
+
# reject
|
63
|
+
true
|
64
|
+
else
|
65
|
+
# keep
|
66
|
+
false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.merge_wheres_and_move_to_front(query)
|
72
|
+
# Map first parts to string
|
73
|
+
query = query.map { |v| v[0] = v[0].to_s; v }
|
74
|
+
has_where = query.find { |v| v[0] == 'find' }
|
75
|
+
|
76
|
+
# if has_find
|
77
|
+
# # merge any finds
|
78
|
+
# merged_find_query = {}
|
79
|
+
# query = query.reject do |query_part|
|
80
|
+
# if query_part[0] == 'find'
|
81
|
+
# # on a find, merge into finds
|
82
|
+
# find_query = query_part[1]
|
83
|
+
# merged_find_query.merge!(find_query) if find_query
|
84
|
+
|
85
|
+
# # reject
|
86
|
+
# true
|
87
|
+
# else
|
88
|
+
# false
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
|
92
|
+
# # Add finds to the front
|
93
|
+
# query.insert(0, ['find', merged_find_query])
|
94
|
+
# else
|
95
|
+
# # No find was done, add it in the first position
|
96
|
+
# query.insert(0, ['find'])
|
97
|
+
# end
|
98
|
+
|
99
|
+
query
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.reject_offset_zero(query)
|
103
|
+
query.reject do |query_part|
|
104
|
+
query_part[0] == 'offset' && query_part[1] == 0
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|