jaql 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 +18 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +2 -0
- data/jaql.gemspec +29 -0
- data/lib/jaql.rb +21 -0
- data/lib/jaql/json_string.rb +27 -0
- data/lib/jaql/resource.rb +28 -0
- data/lib/jaql/sql_generation.rb +14 -0
- data/lib/jaql/sql_generation/active_record_query.rb +18 -0
- data/lib/jaql/sql_generation/associated_column_field.rb +38 -0
- data/lib/jaql/sql_generation/association_field.rb +37 -0
- data/lib/jaql/sql_generation/association_function_field.rb +48 -0
- data/lib/jaql/sql_generation/association_sql.rb +79 -0
- data/lib/jaql/sql_generation/column_field.rb +17 -0
- data/lib/jaql/sql_generation/error_field.rb +16 -0
- data/lib/jaql/sql_generation/field.rb +15 -0
- data/lib/jaql/sql_generation/query.rb +78 -0
- data/lib/jaql/sql_generation/query_parsing.rb +92 -0
- data/lib/jaql/sql_generation/runnable_query.rb +53 -0
- data/lib/jaql/sql_generation/runnable_query_factory_methods.rb +58 -0
- data/lib/jaql/sql_generation/sequel_query.rb +17 -0
- data/lib/jaql/version.rb +3 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 64c4660a85aa4514e7fc0633577549cc5ed20693
|
4
|
+
data.tar.gz: 89641ea69a7cd69181324701b4c31e38064e912b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a2dc3820003fce5aca2acc1f3fdea3deb849f97472ffff32f2258363642fe63ed264b650e03e7386dca6b2097167209710172fd62f0e291e54ff1922a978798f
|
7
|
+
data.tar.gz: f60f9b2b3df488299807ae75fe7a281f8eda99045364e7cc2f4346104ec2fe5910dee9a36ce9d45b42c3af603b692261ad520e93f25b01fb3ab450b3b7f22be2
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Ed Posnak
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Jaql
|
2
|
+
|
3
|
+
JAQL provides fast postgres JSON generation and support for the JSON API Query Language.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'jaql'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install jaql
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
TODO: Write usage instructions here
|
25
|
+
|
26
|
+
## Contributing
|
27
|
+
|
28
|
+
1. Fork it ( https://github.com/[my-github-username]/jaql/fork )
|
29
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
30
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
31
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
32
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/jaql.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jaql/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "jaql"
|
8
|
+
spec.version = Jaql::VERSION
|
9
|
+
spec.authors = ["Ed Posnak"]
|
10
|
+
spec.email = ["ed.posnak@gmail.com"]
|
11
|
+
spec.summary = %q{JSON query language in ruby}
|
12
|
+
spec.description = %q{JSON query language implementation using postgres JSON functions}
|
13
|
+
spec.homepage = "https://github.com/edposnak"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'abstract_method', '~> 1.2'
|
22
|
+
|
23
|
+
spec.add_dependency 'activesupport', '~> 4.2'
|
24
|
+
spec.add_development_dependency 'pg', '~> 0.18'
|
25
|
+
spec.add_dependency 'dart', '~> 0'
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
28
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
29
|
+
end
|
data/lib/jaql.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'abstract_method'
|
2
|
+
|
3
|
+
require 'dart' # just brings in core, other resolvers brought in dynamically as needed
|
4
|
+
|
5
|
+
require 'jaql/version'
|
6
|
+
|
7
|
+
require 'jaql/sql_generation'
|
8
|
+
|
9
|
+
require 'jaql/json_string'
|
10
|
+
require 'jaql/resource'
|
11
|
+
|
12
|
+
|
13
|
+
module Jaql
|
14
|
+
|
15
|
+
module_function
|
16
|
+
|
17
|
+
def resource(scope, options={})
|
18
|
+
Resource.new(scope, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Jaql
|
2
|
+
|
3
|
+
class JSONString
|
4
|
+
|
5
|
+
def initialize(json)
|
6
|
+
raise "#[self.class} initialized with a #{json.class}" unless json.is_a?(String)
|
7
|
+
@json = json
|
8
|
+
end
|
9
|
+
|
10
|
+
# specifying :json format makes Rails and Grape call #to_json on any object it gets back, including JSON
|
11
|
+
# strings, so we just wrap the json in a JSONString that returns it when to_json is called
|
12
|
+
# NB: this method ignores all args and returns the wrapped string as-is
|
13
|
+
def to_json(*args)
|
14
|
+
@json
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_str
|
18
|
+
@json
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
@json
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Jaql
|
2
|
+
# Resource wraps a scope, which can be an ActiveRecord::Relation, Sequel dataset, table name, ORM model instance, etc.
|
3
|
+
# It provides index and show methods that produce JSON for the scope.
|
4
|
+
class Resource
|
5
|
+
attr_reader :scope
|
6
|
+
private :scope
|
7
|
+
|
8
|
+
class NotFoundError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO infer table from string/symbol, e.g. Jaql.resource(:users).show(params[:query])
|
12
|
+
|
13
|
+
# @param [Hash] options
|
14
|
+
# @param [Object] scope defines the scope to search
|
15
|
+
def initialize(scope, options={})
|
16
|
+
@scope = scope
|
17
|
+
end
|
18
|
+
|
19
|
+
def index(query_spec={})
|
20
|
+
JSONString.new SqlGeneration::RunnableQuery.for(scope, query_spec).json_array
|
21
|
+
end
|
22
|
+
|
23
|
+
def show(query_spec={})
|
24
|
+
JSONString.new SqlGeneration::RunnableQuery.for(scope, query_spec).json_row
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'sql_generation/field'
|
2
|
+
require_relative 'sql_generation/association_sql.rb'
|
3
|
+
require_relative 'sql_generation/associated_column_field'
|
4
|
+
require_relative 'sql_generation/association_field'
|
5
|
+
require_relative 'sql_generation/association_function_field'
|
6
|
+
require_relative 'sql_generation/column_field'
|
7
|
+
require_relative 'sql_generation/error_field'
|
8
|
+
|
9
|
+
require_relative 'sql_generation/query_parsing'
|
10
|
+
require_relative 'sql_generation/query'
|
11
|
+
require_relative 'sql_generation/runnable_query_factory_methods'
|
12
|
+
require_relative 'sql_generation/runnable_query'
|
13
|
+
require_relative 'sql_generation/sequel_query'
|
14
|
+
require_relative 'sql_generation/active_record_query'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
# A RunnableQuery extends Query with the ability to produce JSON from postgres by applying spec to scope
|
4
|
+
class ActiveRecordQuery < RunnableQuery
|
5
|
+
private
|
6
|
+
|
7
|
+
def scope_selected_sql
|
8
|
+
# TODO implement client-supplied scopes (where, order, limit) at outer layer
|
9
|
+
scope.select(fields_sql).to_sql
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(sql_to_run, output_col)
|
13
|
+
scope.connection.execute(sql_to_run).first[output_col.to_s]
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
# Allows for creation of fields from some column on some association, e.g. creator.last_name
|
4
|
+
class AssociatedColumnField < Field
|
5
|
+
include AssociationSQL
|
6
|
+
|
7
|
+
attr_reader :association, :column_name, :display_name
|
8
|
+
private :association, :column_name, :display_name
|
9
|
+
|
10
|
+
def initialize(association, column_name, display_name=nil, subquery=nil)
|
11
|
+
@association = association
|
12
|
+
@column_name = column_name
|
13
|
+
@display_name = display_name
|
14
|
+
@subquery = subquery
|
15
|
+
@associated_table_alias = subquery.table_name_alias
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_sql
|
19
|
+
[comment_sql, field_sql].join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def comment_sql
|
25
|
+
"-- #{association.associated_table}.#{column_name} (from #{association.type} #{association.name})"
|
26
|
+
end
|
27
|
+
|
28
|
+
def field_sql
|
29
|
+
select_sql = "SELECT #{table_name_sql(association)}.#{quote column_name} AS #{quote(display_name)}"
|
30
|
+
cte = "#{select_sql}\n #{from_sql(association)}\n #{scope_sql(association)}"
|
31
|
+
|
32
|
+
# return the column value if the association is *_to_one, otherwise return an array
|
33
|
+
association.to_one? ? "(#{cte})" : "(SELECT array(#{cte}) AS #{display_name})"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
class AssociationField < Field
|
4
|
+
include AssociationSQL
|
5
|
+
|
6
|
+
attr_reader :association, :display_name, :subquery
|
7
|
+
private :association, :display_name, :subquery
|
8
|
+
|
9
|
+
|
10
|
+
# e.g. child_undo_broadcast_ass, :child_undo_broadcast_id, json: [:id, :start_time]
|
11
|
+
def initialize(association, display_name=nil, subquery=nil)
|
12
|
+
@association = association
|
13
|
+
@display_name = display_name
|
14
|
+
@subquery = subquery
|
15
|
+
@associated_table_alias = subquery.table_name_alias
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_sql
|
19
|
+
[comment_sql, field_sql].join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def comment_sql
|
25
|
+
"-- #{association.type} #{association.name} (#{association.associated_table})"
|
26
|
+
end
|
27
|
+
|
28
|
+
def field_sql
|
29
|
+
select_sql = "SELECT #{subquery.fields_sql}"
|
30
|
+
cte = "#{select_sql}\n #{from_sql(association)}\n #{scope_sql(association, subquery.scope_options)}"
|
31
|
+
return_type = association.to_one? ? Query::ROW_RETURN_TYPE : Query::ARRAY_RETURN_TYPE
|
32
|
+
field_sql = subquery.json_sql(cte, display_name || association.name, return_type)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
# Allows for creation of fields from some column on some association, e.g. creator.last_name
|
4
|
+
class AssociationFunctionField < Field
|
5
|
+
|
6
|
+
include AssociationSQL
|
7
|
+
|
8
|
+
COUNT_FUNCTION = 'count'
|
9
|
+
EXISTS_FUNCTION = 'exists'
|
10
|
+
|
11
|
+
SUPPORTED_FUNCTIONS = [COUNT_FUNCTION, EXISTS_FUNCTION]
|
12
|
+
def self.supports?(function_name)
|
13
|
+
SUPPORTED_FUNCTIONS.include?(function_name.downcase)
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
attr_reader :association, :function_name, :display_name
|
18
|
+
private :association, :function_name, :display_name
|
19
|
+
|
20
|
+
def initialize(association, function_name, display_name=nil, subquery=nil)
|
21
|
+
@association = association
|
22
|
+
@function_name = function_name.to_s.downcase
|
23
|
+
@display_name = display_name
|
24
|
+
@subquery = subquery
|
25
|
+
@associated_table_alias = subquery.table_name_alias
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_sql
|
29
|
+
[comment_sql, field_sql].join("\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def comment_sql
|
35
|
+
"-- #{association.associated_table}.#{function_name} (from #{association.type} #{association.name})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def field_sql
|
39
|
+
case function_name
|
40
|
+
when COUNT_FUNCTION
|
41
|
+
"(SELECT COUNT(*) AS #{quote(display_name)}\n #{from_sql(association)}\n #{scope_sql(association)})"
|
42
|
+
when EXISTS_FUNCTION
|
43
|
+
"(SELECT EXISTS (SELECT * #{from_sql(association)}\n #{scope_sql(association, limit: 1)} ) AS #{quote(display_name)})"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
module AssociationSQL
|
4
|
+
# TODO the associations themselves should provide this SQL, and lean on the ORMs and all their nifty features to
|
5
|
+
# produce it. This will address the more complicated :through associations involving long chains of all types of
|
6
|
+
# associations
|
7
|
+
|
8
|
+
# includers must respond_to:
|
9
|
+
# @associated_table_alias
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def table_name_sql(association)
|
14
|
+
quote(@associated_table_alias || association.associated_table)
|
15
|
+
end
|
16
|
+
|
17
|
+
def from_sql(association)
|
18
|
+
ass_table = "#{quote association.associated_table} #{@associated_table_alias ? " #{quote @associated_table_alias}" : ''}"
|
19
|
+
join_table = quote(association.join_table) if is_join?(association)
|
20
|
+
"FROM #{[ass_table, join_table].compact.join(', ')}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def scope_sql(association, options={})
|
24
|
+
ass_scope = association.scope
|
25
|
+
client_scope = options.symbolize_keys
|
26
|
+
sql = "WHERE (#{join_cond_sql(association)})"
|
27
|
+
|
28
|
+
# Client WHERE combines with association WHERE
|
29
|
+
if client_where = client_scope[:where]
|
30
|
+
sql << " AND (#{client_where})"
|
31
|
+
end
|
32
|
+
if ass_where = ass_scope[:where]
|
33
|
+
sql << " AND (#{ass_where})"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Client provided ORDER and LIMIT override that of the association
|
37
|
+
if the_order = client_scope[:order] || ass_scope[:order]
|
38
|
+
sql << " ORDER BY (#{the_order})"
|
39
|
+
end
|
40
|
+
|
41
|
+
if ass_limit = client_scope[:limit] || ass_scope[:limit]
|
42
|
+
sql << " LIMIT (#{ass_limit})"
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO OFFSET etc.
|
46
|
+
sql
|
47
|
+
end
|
48
|
+
|
49
|
+
# super private
|
50
|
+
|
51
|
+
def is_join?(ass)
|
52
|
+
ass.respond_to?(:join_table)
|
53
|
+
end
|
54
|
+
|
55
|
+
def join_cond_sql(association)
|
56
|
+
if @associated_table_alias
|
57
|
+
# TODO clean up this ugly mess
|
58
|
+
case association
|
59
|
+
when Dart::ManyToManyAssociation
|
60
|
+
*ass_chain, target_ass = association.join_associations
|
61
|
+
ass_chain.map(&method(:join_cond_sql_for_direct)).join(' AND ') + " AND #{join_cond_sql_for_direct(target_ass, nil, @associated_table_alias)}"
|
62
|
+
when Dart::OneToOneAssociation, Dart::OneToManyAssociation
|
63
|
+
join_cond_sql_for_direct(association, @associated_table_alias, nil)
|
64
|
+
when Dart::ManyToOneAssociation
|
65
|
+
join_cond_sql_for_direct(association, nil, @associated_table_alias)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
ass_chain = is_join?(association) ? association.join_associations : [association]
|
69
|
+
ass_chain.map(&method(:join_cond_sql_for_direct)).join(' AND ')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def join_cond_sql_for_direct(ass, child_table_alias=nil, parent_table_alias=nil)
|
74
|
+
"#{quote(child_table_alias || ass.child_table)}.#{quote ass.foreign_key} = #{quote(parent_table_alias || ass.parent_table)}.#{quote ass.primary_key}"
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
class ColumnField < Field
|
4
|
+
attr_reader :table_name, :column_name, :display_name
|
5
|
+
private :table_name, :column_name, :display_name
|
6
|
+
|
7
|
+
def initialize(table_name, column_name, display_name=nil, subquery_spec=nil)
|
8
|
+
@table_name, @column_name = table_name, column_name
|
9
|
+
@display_name = display_name if column_name != display_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_sql
|
13
|
+
"#{quote table_name}.#{quote column_name}#{display_name && " AS #{quote(display_name)}"}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
class Query
|
4
|
+
include QueryParsing
|
5
|
+
|
6
|
+
attr_reader :table_name_alias # public
|
7
|
+
attr_reader :run_context, :spec, :resolver
|
8
|
+
private :run_context, :spec, :resolver
|
9
|
+
|
10
|
+
def initialize(run_context, spec, resolver, table_name_alias=nil)
|
11
|
+
@run_context = run_context or fail "#{self.class} must be initialized with a run_context"
|
12
|
+
@resolver = resolver or fail "#{self.class} must be initialized with a resolver"
|
13
|
+
@table_name_alias = table_name_alias
|
14
|
+
|
15
|
+
# TODO deep stringify keys when spec is a hash
|
16
|
+
hash_spec = spec.is_a?(String) ? JSON.parse(spec) : spec || {}
|
17
|
+
validate!(hash_spec)
|
18
|
+
@spec = hash_spec
|
19
|
+
end
|
20
|
+
|
21
|
+
ARRAY_RETURN_TYPE = :array
|
22
|
+
ROW_RETURN_TYPE = :row
|
23
|
+
|
24
|
+
def json_sql(cte, display_name, return_type)
|
25
|
+
display_name or raise "display_name cannot be blank"
|
26
|
+
|
27
|
+
rel = run_context.tmp_relation_name
|
28
|
+
select_sql = case return_type
|
29
|
+
when ARRAY_RETURN_TYPE
|
30
|
+
"coalesce(json_agg(#{rel}), '[]'::JSON)"
|
31
|
+
when ROW_RETURN_TYPE
|
32
|
+
"row_to_json(#{rel})"
|
33
|
+
# uncomment this to return {} instead of nil for empty rows
|
34
|
+
# "coalesce(row_to_json(#{rel}), '{}'::JSON)"
|
35
|
+
else
|
36
|
+
fail "unknown return type: '#{return_type}'"
|
37
|
+
end
|
38
|
+
|
39
|
+
%Q{( SELECT #{select_sql} AS "#{display_name}" FROM (#{cte}) #{rel} )}
|
40
|
+
end
|
41
|
+
|
42
|
+
def json_array_sql(cte, display_name)
|
43
|
+
json_sql(cte, display_name, ARRAY_RETURN_TYPE)
|
44
|
+
end
|
45
|
+
|
46
|
+
def json_row_sql(cte, display_name)
|
47
|
+
json_sql(cte, display_name, ROW_RETURN_TYPE)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fields_sql
|
51
|
+
return "#{query_table_name}.*" if fields.empty?
|
52
|
+
|
53
|
+
fields.map {|field| field.to_sql}.join(",\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
def scope_options
|
57
|
+
spec.slice(*ASSOCIATION_SCOPE_OPTION_KEYS)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def query_table_name
|
63
|
+
# defaults to the real table name, overridden by set_table_name
|
64
|
+
@query_table_name ||= @table_name_alias || resolver.table_name
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate!(spec)
|
68
|
+
spec.keys.all? {|k| k.is_a?(String)} or raise "spec containing non-string keys passed to #{self.class}.new. Currently spec must be a JSON hash"
|
69
|
+
end
|
70
|
+
|
71
|
+
def fields
|
72
|
+
@fields ||= parse_fields
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
module QueryParsing
|
4
|
+
|
5
|
+
class InvalidQuery < StandardError ; end
|
6
|
+
|
7
|
+
# includers must respond_to:
|
8
|
+
# query_table_name
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# JAQL Protocol
|
13
|
+
# TODO make all of these KEYS case-insensitive
|
14
|
+
JSON_KEY = 'json'.freeze
|
15
|
+
FROM_KEY = 'from'.freeze
|
16
|
+
WHERE_KEY = 'where'.freeze
|
17
|
+
ORDER_KEY = 'order'.freeze
|
18
|
+
LIMIT_KEY = 'limit'.freeze
|
19
|
+
ASSOCIATION_SCOPE_OPTION_KEYS = [WHERE_KEY, ORDER_KEY, LIMIT_KEY]
|
20
|
+
|
21
|
+
# parses a spec into a list of Fields, each of which may contain their own lists of fields
|
22
|
+
def parse_fields
|
23
|
+
result = []
|
24
|
+
|
25
|
+
# TODO allow
|
26
|
+
# topics: { } # without from or json
|
27
|
+
# members: { from: :users } # without json
|
28
|
+
if json = spec[JSON_KEY]
|
29
|
+
json.each do |field|
|
30
|
+
case field
|
31
|
+
when String, Symbol
|
32
|
+
result << column_or_association(field)
|
33
|
+
when Hash
|
34
|
+
field.each do |display_name, subquery_or_real_name|
|
35
|
+
case subquery_or_real_name
|
36
|
+
when String, Symbol
|
37
|
+
real_name = subquery_or_real_name
|
38
|
+
result << column_or_association(real_name, display_name)
|
39
|
+
when Hash
|
40
|
+
subquery_spec = subquery_or_real_name
|
41
|
+
# peek into the subquery to get the real association name if different from display name
|
42
|
+
if from_name = subquery_spec[FROM_KEY]
|
43
|
+
ass, col = from_name.split('.')
|
44
|
+
if col # get the value from ass.col
|
45
|
+
result << association_column_or_function(ass, col, display_name, subquery_spec)
|
46
|
+
else # from_name is the name of the column or association
|
47
|
+
result << column_or_association(from_name, display_name, subquery_spec)
|
48
|
+
end
|
49
|
+
else # display_name is the name of the column or association
|
50
|
+
result << column_or_association(display_name, nil, subquery_spec)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
else
|
55
|
+
raise InvalidQuery.new("json field '#{field}' is a #{field.class}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
def association_column_or_function(ass_name, col_name, display_name, subquery_spec)
|
64
|
+
if association = resolver.association_for(ass_name)
|
65
|
+
field_class = AssociationFunctionField.supports?(col_name) ? AssociationFunctionField : AssociatedColumnField
|
66
|
+
field_class.new(association, col_name, display_name, build_subquery(association, subquery_spec))
|
67
|
+
else
|
68
|
+
ErrorField.new "unknown association '#{query_table_name}.#{ass_name}' (#{display_name}: #{ass_name}.#{col_name})"
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
def column_or_association(real_name, display_name=nil, subquery_spec=nil)
|
74
|
+
if column_name = resolver.column_for(real_name)
|
75
|
+
ColumnField.new(query_table_name, column_name, display_name)
|
76
|
+
elsif association = resolver.association_for(real_name)
|
77
|
+
AssociationField.new(association, display_name, build_subquery(association, subquery_spec))
|
78
|
+
else
|
79
|
+
ErrorField.new "unknown column or association '#{query_table_name}.#{real_name}' (#{display_name})"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_subquery(association, subquery_spec)
|
84
|
+
new_resolver = resolver.build_from_association(association)
|
85
|
+
# TODO alias any join tables in the association that are the same as resolver.table_name
|
86
|
+
table_alias = "#{association.associated_table}_#{run_context.tmp_relation_name}" if association.associated_table == resolver.table_name
|
87
|
+
Query.new(run_context, subquery_spec, new_resolver, table_alias)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
# A RunnableQuery extends Query with the ability to produce JSON from postgres by applying spec to scope
|
4
|
+
class RunnableQuery < Query
|
5
|
+
extend RunnableQueryFactoryMethods
|
6
|
+
|
7
|
+
abstract_method :scope_selected_sql, :run
|
8
|
+
|
9
|
+
attr_reader :scope
|
10
|
+
private :scope
|
11
|
+
|
12
|
+
def initialize(scope, spec, resolver)
|
13
|
+
@scope = scope
|
14
|
+
|
15
|
+
super(Context.new, spec, resolver)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run the query and produce JSON
|
19
|
+
JSON_RESULT_COL_NAME = 'json_data'.freeze
|
20
|
+
|
21
|
+
def json_array
|
22
|
+
run_returning ARRAY_RETURN_TYPE
|
23
|
+
end
|
24
|
+
|
25
|
+
def json_row
|
26
|
+
run_returning ROW_RETURN_TYPE
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_returning(return_type)
|
30
|
+
select_sql = scope_selected_sql
|
31
|
+
|
32
|
+
sql_to_run = json_sql(select_sql, JSON_RESULT_COL_NAME, return_type)
|
33
|
+
puts "\n\n****************************** sql_to_run:\n#{sql_to_run} \n"
|
34
|
+
run(sql_to_run, JSON_RESULT_COL_NAME)
|
35
|
+
end
|
36
|
+
|
37
|
+
# The context of a particular run
|
38
|
+
# Currently just a temporary relation name generator
|
39
|
+
class Context
|
40
|
+
def initialize(prefix=nil)
|
41
|
+
@prefix = prefix || 'r'
|
42
|
+
@relation_num = 0
|
43
|
+
end
|
44
|
+
|
45
|
+
def tmp_relation_name()
|
46
|
+
@relation_num += 1
|
47
|
+
"#{@prefix}#{@relation_num}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
module RunnableQueryFactoryMethods
|
4
|
+
def for(scope, query_spec)
|
5
|
+
scope_class = scope.class
|
6
|
+
|
7
|
+
case scope_class.name
|
8
|
+
|
9
|
+
when 'Sequel::Postgres::Dataset'
|
10
|
+
if scope.respond_to?(:model) # Sequel::Postgres::Dataset with Sequel::Model
|
11
|
+
sequel_model_query(scope, query_spec, scope.model)
|
12
|
+
else # Sequel::Postgres::Dataset (no model)
|
13
|
+
sequel_table_query(scope, query_spec, scope.first_source)
|
14
|
+
end
|
15
|
+
|
16
|
+
when 'ActiveRecord::Relation'
|
17
|
+
active_record_model_query(scope, query_spec, scope.klass)
|
18
|
+
|
19
|
+
else # could be an ORM model instance
|
20
|
+
if scope_class.ancestors.map(&:name).include?('Sequel::Model')
|
21
|
+
sequel_model_query(scope, query_spec, scope_class)
|
22
|
+
elsif scope_class.ancestors.map(&:name).include?('ActiveRecord::Base')
|
23
|
+
active_record_model_query(scope, query_spec, scope_class)
|
24
|
+
else
|
25
|
+
fail "cannot determine resolver for scope with type '#{scope_class}'"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def active_record_model_query(scope, query_spec, model)
|
33
|
+
unless defined?(Dart::Reflection::ActiveRecordModel::Resolver)
|
34
|
+
require 'dart/active_record_model_reflection'
|
35
|
+
end
|
36
|
+
resolver = Dart::Reflection::ActiveRecordModel::Resolver.new(model)
|
37
|
+
ActiveRecordQuery.new(scope, query_spec, resolver)
|
38
|
+
end
|
39
|
+
|
40
|
+
def sequel_table_query(scope, query_spec, table)
|
41
|
+
unless defined?(Dart::Reflection::SequelTable::Resolver)
|
42
|
+
require 'dart/sequel_table_reflection'
|
43
|
+
end
|
44
|
+
resolver = Dart::Reflection::SequelTable::Resolver.new(table)
|
45
|
+
SequelQuery.new(scope, query_spec, resolver)
|
46
|
+
end
|
47
|
+
|
48
|
+
def sequel_model_query(scope, query_spec, model)
|
49
|
+
unless defined?(Dart::Reflection::SequelModel::Resolver)
|
50
|
+
require 'dart/sequel_model_reflection'
|
51
|
+
end
|
52
|
+
resolver = Dart::Reflection::SequelModel::Resolver.new(model)
|
53
|
+
SequelQuery.new(scope, query_spec, resolver)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Jaql
|
2
|
+
module SqlGeneration
|
3
|
+
class SequelQuery < RunnableQuery
|
4
|
+
private
|
5
|
+
|
6
|
+
def scope_selected_sql
|
7
|
+
# TODO implement client-supplied scopes (where, order, limit) at outer layer
|
8
|
+
scope.select(Sequel.lit(fields_sql)).sql
|
9
|
+
end
|
10
|
+
|
11
|
+
def run(sql_to_run, output_col)
|
12
|
+
scope.db[sql_to_run].first[output_col.to_sym]
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/jaql/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jaql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ed Posnak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: abstract_method
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pg
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.18'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.18'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dart
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.7'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.7'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
description: JSON query language implementation using postgres JSON functions
|
98
|
+
email:
|
99
|
+
- ed.posnak@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- Gemfile
|
106
|
+
- LICENSE.txt
|
107
|
+
- README.md
|
108
|
+
- Rakefile
|
109
|
+
- jaql.gemspec
|
110
|
+
- lib/jaql.rb
|
111
|
+
- lib/jaql/json_string.rb
|
112
|
+
- lib/jaql/resource.rb
|
113
|
+
- lib/jaql/sql_generation.rb
|
114
|
+
- lib/jaql/sql_generation/active_record_query.rb
|
115
|
+
- lib/jaql/sql_generation/associated_column_field.rb
|
116
|
+
- lib/jaql/sql_generation/association_field.rb
|
117
|
+
- lib/jaql/sql_generation/association_function_field.rb
|
118
|
+
- lib/jaql/sql_generation/association_sql.rb
|
119
|
+
- lib/jaql/sql_generation/column_field.rb
|
120
|
+
- lib/jaql/sql_generation/error_field.rb
|
121
|
+
- lib/jaql/sql_generation/field.rb
|
122
|
+
- lib/jaql/sql_generation/query.rb
|
123
|
+
- lib/jaql/sql_generation/query_parsing.rb
|
124
|
+
- lib/jaql/sql_generation/runnable_query.rb
|
125
|
+
- lib/jaql/sql_generation/runnable_query_factory_methods.rb
|
126
|
+
- lib/jaql/sql_generation/sequel_query.rb
|
127
|
+
- lib/jaql/version.rb
|
128
|
+
homepage: https://github.com/edposnak
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.4.5
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: JSON query language in ruby
|
152
|
+
test_files: []
|