volt-sql 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|