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 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
@@ -0,0 +1,18 @@
1
+ .ruby-version
2
+ /.idea/
3
+ /vendor/
4
+ /.bundle/
5
+ /.yardoc
6
+ /Gemfile.lock
7
+ /_yardoc/
8
+ /coverage/
9
+ /doc/
10
+ /pkg/
11
+ /spec/reports/
12
+ /tmp/
13
+ *.bundle
14
+ *.so
15
+ *.o
16
+ *.a
17
+ mkmf.log
18
+ /TODO.txt
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jaql.gemspec
4
+ gemspec
5
+
6
+ gem 'dart', path: '../dart'
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
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,16 @@
1
+ module Jaql
2
+ module SqlGeneration
3
+ class ErrorField < Field
4
+ attr_reader :message
5
+
6
+ def initialize(message)
7
+ @message = message
8
+ end
9
+
10
+ def to_sql
11
+ raise message
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,15 @@
1
+ module Jaql
2
+ module SqlGeneration
3
+
4
+ class Field
5
+ abstract_method :to_sql
6
+
7
+ private
8
+
9
+ def quote(id)
10
+ "\"#{id}\""
11
+ end
12
+ end
13
+
14
+ end
15
+ 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
@@ -0,0 +1,3 @@
1
+ module Jaql
2
+ VERSION = "0.0.1"
3
+ end
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: []