hamster-web 0.1.0.pre.2

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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +61 -0
  4. data/exe/hamster +24 -0
  5. data/lib/hamster/build/manifest.rb +53 -0
  6. data/lib/hamster/build/postgres_compatibility.rb +38 -0
  7. data/lib/hamster/build/query_accessors.rb +73 -0
  8. data/lib/hamster/build/view_compiler.rb +239 -0
  9. data/lib/hamster/cli.rb +166 -0
  10. data/lib/hamster/console.rb +34 -0
  11. data/lib/hamster/data/persistence.rb +152 -0
  12. data/lib/hamster/data/query.rb +106 -0
  13. data/lib/hamster/data/read/compiler.rb +101 -0
  14. data/lib/hamster/data/read/decoder.rb +101 -0
  15. data/lib/hamster/data/read/nodes.rb +118 -0
  16. data/lib/hamster/data/read/relation.rb +198 -0
  17. data/lib/hamster/data/read/schema.rb +40 -0
  18. data/lib/hamster/data/read/validator.rb +273 -0
  19. data/lib/hamster/data/record.rb +81 -0
  20. data/lib/hamster/data/table.rb +90 -0
  21. data/lib/hamster/data.rb +21 -0
  22. data/lib/hamster/database/admin.rb +39 -0
  23. data/lib/hamster/database/configuration.rb +84 -0
  24. data/lib/hamster/database/connection.rb +168 -0
  25. data/lib/hamster/database/database.rb +92 -0
  26. data/lib/hamster/database/errors.rb +21 -0
  27. data/lib/hamster/database/pool.rb +98 -0
  28. data/lib/hamster/database/postgres.rb +66 -0
  29. data/lib/hamster/database/result.rb +30 -0
  30. data/lib/hamster/schema_validation.rb +12 -0
  31. data/lib/hamster/web/action/base.rb +39 -0
  32. data/lib/hamster/web/application.rb +23 -0
  33. data/lib/hamster/web/context.rb +25 -0
  34. data/lib/hamster/web/html.rb +80 -0
  35. data/lib/hamster/web/http.rb +34 -0
  36. data/lib/hamster/web/instrumentation.rb +28 -0
  37. data/lib/hamster/web/router.rb +27 -0
  38. data/lib/hamster/web/routing/base.rb +131 -0
  39. data/lib/hamster/web/version.rb +7 -0
  40. data/lib/hamster/web/view/form.rb +119 -0
  41. data/lib/hamster/web/view.rb +196 -0
  42. data/lib/hamster/web.rb +13 -0
  43. data/templates/app/.gitignore +5 -0
  44. data/templates/app/AGENTS.md +8 -0
  45. data/templates/app/Gemfile +3 -0
  46. data/templates/app/README.md +32 -0
  47. data/templates/app/app/routes/home/index/action.rb +14 -0
  48. data/templates/app/app/routes/home/index/view.html.erb +2 -0
  49. data/templates/app/app/routes/mappings.rb +8 -0
  50. data/templates/app/app/views/layouts/application.html.erb +3 -0
  51. data/templates/app/bin/hamster +4 -0
  52. data/templates/app/bin/server +8 -0
  53. data/templates/app/config/application.rb +23 -0
  54. data/templates/app/config/commands.rb +16 -0
  55. data/templates/app/config/database.rb +20 -0
  56. data/templates/app/config/hamster.json +1 -0
  57. data/templates/app/db/migrations/.keep +0 -0
  58. data/templates/app/spin.lock +6 -0
  59. data/templates/app/spin.toml +2 -0
  60. metadata +133 -0
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hamster
4
+ module Read
5
+ class Error < StandardError; end
6
+ class ValidationError < Error; end
7
+ class DecodeError < Error; end
8
+
9
+ TYPES = { integer: 23, bigint: 20, text: 25, boolean: 16, timestamptz: 1184 }.freeze
10
+
11
+ def self.snapshot(value)
12
+ case value
13
+ when Array then value.map { |item| snapshot(item) }.freeze
14
+ when Hash
15
+ result = {}
16
+ value.each { |key, item| result[snapshot(key)] = snapshot(item) }
17
+ result.freeze
18
+ when String, Time then value.dup.freeze
19
+ else value
20
+ end
21
+ end
22
+
23
+ def self.structural(value)
24
+ case value
25
+ when Node, Source then value.key
26
+ when Array then value.map { |item| structural(item) }
27
+ when Hash
28
+ result = {}
29
+ value.each { |key, item| result[key] = structural(item) }
30
+ result
31
+ else value
32
+ end
33
+ end
34
+
35
+ def self.identifier(value)
36
+ name = value.to_s
37
+ raise ValidationError, "Invalid identifier #{name.inspect}" if name.empty? || name.include?("\0")
38
+ name.dup.freeze
39
+ end
40
+
41
+ class Source
42
+ attr_reader :row_class, :name, :schema, :label, :columns
43
+ def initialize(row_class, label = nil)
44
+ @row_class = row_class
45
+ @name = row_class.table_name
46
+ @schema = row_class.schema_name
47
+ @label = Read.identifier(label || @name)
48
+ @columns = Read.snapshot(row_class.columns)
49
+ freeze
50
+ end
51
+
52
+ def [](name)
53
+ Node.new(:column, [self, name.to_sym])
54
+ end
55
+
56
+ def all
57
+ Relation.new(self)
58
+ end
59
+
60
+ def key
61
+ [row_class, schema, name, label]
62
+ end
63
+ end
64
+
65
+ # One expression representation is shared by validation, compilation and decoding.
66
+ class Node
67
+ attr_reader :kind, :args, :options
68
+ def initialize(kind, args = [], options = {})
69
+ @kind = kind
70
+ @args = Read.snapshot(args)
71
+ @options = Read.snapshot(options)
72
+ freeze
73
+ end
74
+
75
+ def key
76
+ [kind, Read.structural(args), Read.structural(options)]
77
+ end
78
+
79
+ def eq(value); compare(:eq, value); end
80
+ def not_eq(value); compare(:not_eq, value); end
81
+ def gt(value); compare(:gt, value); end
82
+ def gte(value); compare(:gte, value); end
83
+ def lt(value); compare(:lt, value); end
84
+ def lte(value); compare(:lte, value); end
85
+ def ilike(pattern); compare(:ilike, pattern); end
86
+ def in(values); Node.new(:in, [self, values]); end
87
+ def not_in(values); Node.new(:not_in, [self, values]); end
88
+ def as(name); Node.new(:as, [self, Read.identifier(name)]); end
89
+ def asc; Node.new(:order, [self, :asc]); end
90
+ def desc; Node.new(:order, [self, :desc]); end
91
+ def and(other); Node.new(:and, [self, other]); end
92
+ def or(other); Node.new(:or, [self, other]); end
93
+ def not; Node.new(:not, [self]); end
94
+
95
+ def compare(operator, value)
96
+ Node.new(operator, [self, value.is_a?(Node) ? value : Node.new(:value, [value])])
97
+ end
98
+ end
99
+ end
100
+
101
+ module SQL
102
+ def self.count(column = nil, distinct: false)
103
+ Read::Node.new(:count, column.nil? ? [] : [column], { distinct: distinct })
104
+ end
105
+
106
+ def self.output(name)
107
+ Read::Node.new(:output, [Read.identifier(name)])
108
+ end
109
+
110
+ def self.bind(value)
111
+ Read::Node.new(:value, [value])
112
+ end
113
+
114
+ def self.raw(parts, dependencies:, type:, nullable:, aggregate: false)
115
+ Read::Node.new(:raw, parts, { dependencies: dependencies, type: type, nullable: nullable, aggregate: aggregate })
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hamster
4
+ class Relation
5
+ attr_reader :source, :state
6
+
7
+ def initialize(source, state = {})
8
+ @source = source
9
+ @state = Read.snapshot({ select: [], explicit: false, joins: [], where: [], group: [],
10
+ having: [], order: [], distinct: false, limit: nil, offset: nil, page: nil,
11
+ identity: "anonymous" }.merge(state))
12
+ freeze
13
+ end
14
+
15
+ def all; self; end
16
+ def named(identity); change(identity: identity.to_s); end
17
+ def select(*items); change(select: state[:select] + expressions(items), explicit: true); end
18
+ def reselect(*items); change(select: expressions(items), explicit: true); end
19
+ def where(condition); change(where: state[:where] + conditions(condition)); end
20
+ def having(condition); change(having: state[:having] + conditions(condition)); end
21
+ def group(*items); change(group: state[:group] + expressions(items)); end
22
+ def order(*items); change(order: state[:order] + orderings(items)); end
23
+ def reorder(*items); change(order: orderings(items)); end
24
+ def distinct(enabled = true); change(distinct: enabled); end
25
+ def limit(value); change(limit: value); end
26
+ def offset(value); change(offset: value, page: nil); end
27
+
28
+ def page(number)
29
+ unless number.is_a?(Integer) && number > 0
30
+ raise Read::ValidationError, "#{state[:identity]}: PAGE: use a positive integer starting at 1"
31
+ end
32
+ change(page: number, offset: nil)
33
+ end
34
+
35
+ def joins(other, on:)
36
+ join(other, on, :inner)
37
+ end
38
+
39
+ def left_joins(other, on:)
40
+ join(other, on, :left)
41
+ end
42
+
43
+ def compile
44
+ Read::Compiler.new(Read::Validator.new(resolve_page).validate).compile
45
+ end
46
+
47
+ def to_sql; compile.sql; end
48
+ def binds; compile.binds; end
49
+
50
+ def load(connection: nil)
51
+ Instrumentation.instrument("query.load", attributes: {"query.name" => state[:identity]}) do
52
+ compiled = compile
53
+ result = (connection || Record.connection).execute(compiled.sql, compiled.binds)
54
+ Read::Decoder.new(compiled).decode(result)
55
+ end
56
+ end
57
+
58
+ def count(connection: nil)
59
+ Instrumentation.instrument("query.count", attributes: {"query.name" => state[:identity]}) do
60
+ compiled = Read::Compiler.new(Read::Validator.new(resolve_page).validate).compile(count: true)
61
+ result = (connection || Record.connection).execute(compiled.sql, compiled.binds)
62
+ rows = Read::Decoder.new(compiled).decode(result)
63
+ raise Read::DecodeError, "Count result must contain exactly one row" unless rows.length == 1
64
+ rows.first[:count]
65
+ end
66
+ end
67
+
68
+ # Terminal reads stay lazy until this call and use PostgreSQL for the limit.
69
+ def first(connection: nil)
70
+ return resolve_page.first(connection: connection) unless state[:page].nil?
71
+ compile
72
+ return nil if state[:limit] == 0
73
+
74
+ query = self
75
+ if query.state[:order].empty? && implicit_primary_order?
76
+ query = query.order(source[:id].asc)
77
+ end
78
+ query.limit(query.state[:limit].nil? ? 1 : [query.state[:limit], 1].min)
79
+ .load(connection: connection).first
80
+ end
81
+
82
+ def last(connection: nil)
83
+ return resolve_page.last(connection: connection) unless state[:page].nil?
84
+ compile
85
+ return nil if state[:limit] == 0
86
+
87
+ # Reversing an ordered, offset relation changes which rows the offset skips.
88
+ # Load that already-scoped window to preserve its meaning.
89
+ if !state[:offset].nil? || (state[:order].empty? && !implicit_primary_order?)
90
+ query = state[:order].empty? && implicit_primary_order? ? order(source[:id].asc) : self
91
+ return query.load(connection: connection).last
92
+ end
93
+
94
+ if state[:order].empty? && !state[:limit].nil?
95
+ return order(source[:id].asc).load(connection: connection).last
96
+ end
97
+
98
+ reversed = if state[:order].empty?
99
+ order(source[:id].desc)
100
+ else
101
+ order = state[:order].map do |item|
102
+ direction = item.args[1] == :asc ? :desc : :asc
103
+ Read::Node.new(:order, [item.args[0], direction])
104
+ end
105
+ reorder(*order)
106
+ end
107
+ if reversed.state[:limit].nil?
108
+ reversed.limit(1).load(connection: connection).first
109
+ else
110
+ reversed.load(connection: connection).first
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def validate_page_size!
117
+ unless state[:limit].is_a?(Integer) && state[:limit] > 0
118
+ raise Read::ValidationError, "#{state[:identity]}: PAGE: pagination requires a positive page size; add .limit(size)"
119
+ end
120
+ end
121
+
122
+ def resolve_page
123
+ return self if state[:page].nil?
124
+ validate_page_size!
125
+ query = self
126
+ if state[:order].empty?
127
+ unless implicit_primary_order?
128
+ raise Read::ValidationError, "#{state[:identity]}: PAGE: add an explicit order for grouped, distinct, aggregate, or id-less queries"
129
+ end
130
+ query = order(source[:id].asc)
131
+ end
132
+ query.offset((state[:page] - 1) * state[:limit])
133
+ end
134
+
135
+ def change(changes)
136
+ Relation.new(source, state.merge(changes))
137
+ end
138
+
139
+ def expression(item)
140
+ item.is_a?(Symbol) ? source[item] : item
141
+ end
142
+
143
+ def expressions(items)
144
+ items.flatten.map { |item| expression(item) }
145
+ end
146
+
147
+ def conditions(condition)
148
+ if condition.is_a?(Hash)
149
+ condition.flat_map do |key, value|
150
+ column = source[key]
151
+ if value.is_a?(Range)
152
+ bounds = []
153
+ bounds << column.gte(value.begin) unless value.begin.nil?
154
+ if !value.end.nil?
155
+ bounds << (value.exclude_end? ? column.lt(value.end) : column.lte(value.end))
156
+ end
157
+ bounds
158
+ elsif value.is_a?(Array)
159
+ [column.in(value)]
160
+ else
161
+ [column.eq(value)]
162
+ end
163
+ end
164
+ else
165
+ [condition]
166
+ end
167
+ end
168
+
169
+ def orderings(items)
170
+ items.flatten.flat_map do |item|
171
+ if item.is_a?(Hash)
172
+ item.map { |key, direction| Read::Node.new(:order, [source[key], direction]) }
173
+ else
174
+ node = expression(item)
175
+ [node.is_a?(Read::Node) && node.kind == :order ? node : Read::Node.new(:order, [node, :asc])]
176
+ end
177
+ end
178
+ end
179
+
180
+ def implicit_primary_order?
181
+ id = source.columns[:id]
182
+ id && id[:enabled] && state[:group].empty? && state[:having].empty? && !state[:distinct] &&
183
+ !state[:select].any? { |item| aggregate_expression?(item) } &&
184
+ !state[:order].any? { |item| aggregate_expression?(item) }
185
+ end
186
+
187
+ def aggregate_expression?(item)
188
+ return false unless item.is_a?(Read::Node)
189
+ return true if item.kind == :count || (item.kind == :raw && item.options[:aggregate])
190
+ item.args.any? { |argument| aggregate_expression?(argument) }
191
+ end
192
+
193
+ def join(other, on, kind)
194
+ joined = other.is_a?(Read::Source) ? other : other.all.source
195
+ change(joins: state[:joins] + [Read::Node.new(:join, [joined, on], { kind: kind })])
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hamster
4
+ module Read
5
+ class Schema
6
+ def self.validate(connection, tables = Table.registry)
7
+ errors = []
8
+ tables.each do |table|
9
+ catalog = connection.execute(<<~SQL, [table.schema_name, table.table_name]).hashes
10
+ SELECT a.attname AS name, a.atttypid::text AS type_id,
11
+ a.attnotnull::text AS not_null
12
+ FROM pg_catalog.pg_attribute a
13
+ JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
14
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
15
+ WHERE n.nspname = $1 AND c.relname = $2
16
+ AND c.relkind IN ('r', 'p', 'v', 'm', 'f')
17
+ AND a.attnum > 0 AND NOT a.attisdropped
18
+ SQL
19
+ prefix = "#{table.schema_name}.#{table.table_name}"
20
+ errors << "#{prefix}: table is missing" if catalog.empty?
21
+ table.columns.each do |name, definition|
22
+ next unless definition[:enabled]
23
+ actual = catalog.find { |row| row["name"] == name.to_s }
24
+ if actual.nil?
25
+ errors << "#{prefix}.#{name}: column is missing"
26
+ else
27
+ unless actual["type_id"] == TYPES[definition[:type]].to_s
28
+ errors << "#{prefix}.#{name}: incompatible type (expected #{definition[:type]})"
29
+ end
30
+ if !definition[:nullable] && actual["not_null"] != "true"
31
+ errors << "#{prefix}.#{name}: database permits NULL"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ errors
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Hamster
5
+ module Read
6
+ class Validator
7
+ attr_reader :relation, :source, :state, :outputs, :dependencies, :table_rows, :result_class
8
+
9
+ def initialize(relation)
10
+ @relation = relation
11
+ @source = relation.source
12
+ @state = relation.state.dup
13
+ @outputs = []
14
+ @dependencies = []
15
+ @sources = [source]
16
+ @left_sources = []
17
+ @aliases = {}
18
+ end
19
+
20
+ def fail!(message, clause)
21
+ raise ValidationError, "#{state[:identity]}: #{clause}: #{message}"
22
+ end
23
+
24
+ def validate
25
+ state[:joins].each do |join|
26
+ other = join.args[0]
27
+ fail!("duplicate source alias #{other.label}", "JOIN") if @sources.any? { |s| s.label == other.label }
28
+ @sources << other
29
+ @left_sources << other.key if join.options[:kind] == :left
30
+ end
31
+ selections = if state[:explicit]
32
+ state[:select]
33
+ else
34
+ source.columns.select { |_key, spec| spec[:enabled] }.map { |key, _spec| source[key] }
35
+ end
36
+ fail!("empty projection", "SELECT") if selections.empty?
37
+ (state[:declared_fields] || []).each { |node| @aliases[node.args[1]] = node.args[0] }
38
+ declared_aliases = @aliases.dup
39
+ declared_projection = (state[:declared_fields] || []).map do |node|
40
+ resolved = resolve(node, "SELECT", [])
41
+ [output_name(resolved), unwrap(resolved).key]
42
+ end
43
+ selections.each do |node|
44
+ if node.is_a?(Node) && node.kind == :as
45
+ name = node.args[1]
46
+ # Query output references continue to name the declared field even
47
+ # when a runtime projection replaces or aliases that field.
48
+ @aliases[name] = node.args[0] unless declared_aliases.key?(name)
49
+ end
50
+ end
51
+ state[:select] = unique(selections.map do |node|
52
+ resolved = resolve(node, "SELECT", [])
53
+ node.kind == :output ? resolved.as(node.args[0]) : resolved
54
+ end)
55
+ state[:group] = unique(state[:group].map { |node| resolve(node, "GROUP BY", []) })
56
+ [:where, :having, :order].each do |clause|
57
+ state[clause] = state[clause].map { |node| resolve(node, clause.to_s.upcase, []) }
58
+ end
59
+ visible = [source]
60
+ state[:joins] = state[:joins].map do |join|
61
+ visible << join.args[0]
62
+ node = resolve(join.args[1], "ON", [])
63
+ info = inspect_node(node, "ON", visible)
64
+ predicate!(info, "ON")
65
+ Node.new(:join, [join.args[0], node], join.options)
66
+ end
67
+ state[:select].each do |node|
68
+ info = inspect_node(node, "SELECT", @sources)
69
+ name = output_name(node)
70
+ fail!("projection needs an explicit alias", "SELECT") unless name
71
+ fail!("conflicting output #{name}", "SELECT") if outputs.any? { |out| out[:name] == name }
72
+ outputs << { name: name, type: info[:type], nullable: info[:nullable], expression: unwrap(node) }.freeze
73
+ end
74
+ [:where, :having].each do |clause|
75
+ state[clause].each { |node| predicate!(inspect_node(node, clause.to_s.upcase, @sources), clause.to_s.upcase) }
76
+ end
77
+ state[:group].each { |node| inspect_node(node, "GROUP BY", @sources) }
78
+ state[:order].each do |node|
79
+ fail!("unsupported ordering", "ORDER BY") unless node.is_a?(Node) && node.kind == :order
80
+ fail!("direction must be :asc or :desc", "ORDER BY") unless [:asc, :desc].include?(node.args[1])
81
+ inspect_node(node.args[0], "ORDER BY", @sources)
82
+ end
83
+ grouped = !state[:group].empty? || !state[:having].empty? ||
84
+ state[:select].any? { |node| aggregate?(node) } || state[:order].any? { |node| aggregate?(node.args[0]) }
85
+ if grouped
86
+ state[:select].each { |node| check_group(unwrap(node), "SELECT") }
87
+ state[:having].each { |node| check_group(node, "HAVING") }
88
+ state[:order].each { |node| check_group(node.args[0], "ORDER BY") }
89
+ end
90
+ fail!("distinct must be boolean", "DISTINCT") unless [true, false].include?(state[:distinct])
91
+ if state[:distinct]
92
+ state[:order].each do |node|
93
+ unless outputs.any? { |out| out[:expression].key == node.args[0].key }
94
+ fail!("DISTINCT ordering must be selected", "ORDER BY")
95
+ end
96
+ end
97
+ end
98
+ [:limit, :offset].each do |clause|
99
+ value = state[clause]
100
+ unless value.nil? || (value.is_a?(Integer) && value >= 0)
101
+ fail!("must be a nonnegative integer", clause.to_s.upcase)
102
+ end
103
+ end
104
+ @table_rows = state[:joins].empty? && !grouped && state[:select].all? do |node|
105
+ node.kind == :column && node.args[0].key == source.key
106
+ end
107
+ if state[:result_class]
108
+ @result_class = state[:result_class] if outputs.all? do |out|
109
+ declared_projection.include?([out[:name], out[:expression].key])
110
+ end
111
+ @table_rows = false
112
+ end
113
+ @state = Read.snapshot(state)
114
+ @outputs.freeze
115
+ @dependencies.freeze
116
+ self
117
+ end
118
+
119
+ def resolve(node, clause, stack)
120
+ fail!("unsupported expression #{node.class}", clause) unless node.is_a?(Node)
121
+ if node.kind == :output
122
+ name = node.args[0]
123
+ fail!("unknown output #{name}", clause) unless @aliases.key?(name)
124
+ fail!("cyclic output #{name}", clause) if stack.include?(name)
125
+ return resolve(@aliases[name], clause, stack + [name])
126
+ end
127
+ args = node.args.map { |arg| arg.is_a?(Node) ? resolve(arg, clause, stack) : arg }
128
+ Node.new(node.kind, args, node.options)
129
+ end
130
+
131
+ def unique(nodes)
132
+ result = []
133
+ nodes.each { |node| result << node unless result.any? { |old| old.key == node.key } }
134
+ result
135
+ end
136
+
137
+ def unwrap(node)
138
+ node.kind == :as ? node.args[0] : node
139
+ end
140
+
141
+ def output_name(node)
142
+ return node.args[1] if node.kind == :as
143
+ return node.args[1].to_s if node.kind == :column
144
+ return "count" if node.kind == :count
145
+ nil
146
+ end
147
+
148
+ def info(type, nullable, aggregate = false)
149
+ { type: type, nullable: nullable, aggregate: aggregate }
150
+ end
151
+
152
+ def predicate!(metadata, clause)
153
+ fail!("condition must be boolean", clause) unless metadata[:type] == :boolean
154
+ end
155
+
156
+ def inspect_node(node, clause, visible)
157
+ fail!("unsupported expression", clause) unless node.is_a?(Node)
158
+ case node.kind
159
+ when :column
160
+ table, name = node.args
161
+ fail!("source #{table.label} is not visible", clause) unless visible.any? { |s| s.key == table.key }
162
+ definition = table.columns[name]
163
+ fail!("#{table.schema}.#{table.name}.#{name} is undeclared", clause) unless definition
164
+ fail!("#{table.schema}.#{table.name}.#{name} is disabled", clause) unless definition[:enabled]
165
+ dependency = { schema: table.schema, table: table.name, source: table.label, column: name, clause: clause }.freeze
166
+ @dependencies << dependency unless @dependencies.include?(dependency)
167
+ info(definition[:type], definition[:nullable] || @left_sources.include?(table.key))
168
+ when :as
169
+ fail!("projection aliases are only allowed in SELECT", clause) unless clause == "SELECT"
170
+ inspect_node(node.args[0], clause, visible)
171
+ when :value
172
+ value = node.args[0]
173
+ type = case value
174
+ when NilClass then :null
175
+ when Integer then :bigint
176
+ when String then :text
177
+ when TrueClass, FalseClass then :boolean
178
+ when Time then :timestamptz
179
+ else
180
+ case value.class.to_s
181
+ when "DateTime" then :timestamptz
182
+ when "Date" then :date
183
+ else fail!("unsupported bound value #{value.class}", clause)
184
+ end
185
+ end
186
+ info(type, value.nil?)
187
+ when :count
188
+ fail!("aggregate is not allowed", clause) if ["WHERE", "ON", "GROUP BY"].include?(clause)
189
+ fail!("COUNT DISTINCT requires a column", clause) if node.args.empty? && node.options[:distinct]
190
+ fail!("COUNT distinct flag must be boolean", clause) unless [true, false].include?(node.options[:distinct])
191
+ node.args.each do |arg|
192
+ fail!("nested aggregate", clause) if aggregate?(arg)
193
+ inspect_node(arg, clause, visible)
194
+ end
195
+ info(:bigint, false, true)
196
+ when :raw
197
+ type = node.options[:type]
198
+ fail!("unsupported raw type", clause) unless TYPES.key?(type)
199
+ [:nullable, :aggregate].each do |flag|
200
+ fail!("raw #{flag} must be boolean", clause) unless [true, false].include?(node.options[flag])
201
+ end
202
+ if node.options[:aggregate] && ["WHERE", "ON", "GROUP BY"].include?(clause)
203
+ fail!("aggregate is not allowed", clause)
204
+ end
205
+ deps = node.options[:dependencies]
206
+ fail!("raw dependencies must be an array of columns", clause) unless deps.is_a?(Array)
207
+ deps.each do |dep|
208
+ fail!("raw dependency must be a column", clause) unless dep.is_a?(Node) && dep.kind == :column
209
+ inspect_node(dep, clause, visible)
210
+ end
211
+ fail!("raw parts cannot be empty", clause) if node.args.empty?
212
+ node.args.each do |part|
213
+ unless part.is_a?(String) || (part.is_a?(Node) && part.kind == :value)
214
+ fail!("raw parts must be strings or SQL.bind nodes", clause)
215
+ end
216
+ inspect_node(part, clause, visible) if part.is_a?(Node)
217
+ end
218
+ info(type, node.options[:nullable], node.options[:aggregate])
219
+ when :eq, :not_eq, :gt, :gte, :lt, :lte
220
+ left = inspect_node(node.args[0], clause, visible)
221
+ right = inspect_node(node.args[1], clause, visible)
222
+ if right[:type] == :null && ![:eq, :not_eq].include?(node.kind)
223
+ fail!("NULL only supports eq and not_eq", clause)
224
+ end
225
+ compatible!(left, right, node.args[1], clause)
226
+ info(:boolean, right[:type] == :null ? false : left[:nullable] || right[:nullable])
227
+ when :ilike
228
+ left = inspect_node(node.args[0], clause, visible)
229
+ right = inspect_node(node.args[1], clause, visible)
230
+ fail!("ILIKE requires text operands", clause) unless left[:type] == :text && right[:type] == :text
231
+ info(:boolean, left[:nullable] || right[:nullable])
232
+ when :in, :not_in
233
+ left = inspect_node(node.args[0], clause, visible)
234
+ values = node.args[1]
235
+ fail!("membership requires an array", clause) unless values.is_a?(Array)
236
+ values.each do |value|
237
+ rhs = Node.new(:value, [value])
238
+ compatible!(left, inspect_node(rhs, clause, visible), rhs, clause)
239
+ end
240
+ info(:boolean, left[:nullable])
241
+ when :and, :or, :not
242
+ node.args.each { |arg| predicate!(inspect_node(arg, clause, visible), clause) }
243
+ info(:boolean, true)
244
+ else
245
+ fail!("unsupported expression #{node.kind}", clause)
246
+ end
247
+ end
248
+
249
+ def compatible!(left, right, rhs, clause)
250
+ return if right[:type] == :null || left[:type] == right[:type]
251
+ return if [:integer, :bigint].include?(left[:type]) && [:integer, :bigint].include?(right[:type])
252
+ # Bound text is passed with an unspecified PostgreSQL type, allowing e.g. an ID from a route.
253
+ return if rhs.kind == :value && right[:type] == :text
254
+ return if right[:type] == :date && left[:type] == :timestamptz
255
+ fail!("incompatible comparison types", clause)
256
+ end
257
+
258
+ def aggregate?(node)
259
+ return false unless node.is_a?(Node)
260
+ return true if node.kind == :count || (node.kind == :raw && node.options[:aggregate])
261
+ node.args.any? { |arg| arg.is_a?(Node) && aggregate?(arg) }
262
+ end
263
+
264
+ def check_group(node, clause)
265
+ return if state[:group].any? { |group| group.key == node.key }
266
+ return if node.kind == :count || node.kind == :value
267
+ return if node.kind == :raw && node.options[:aggregate]
268
+ fail!("nonaggregate expression must be explicitly grouped", clause) if [:column, :raw].include?(node.kind)
269
+ node.args.each { |arg| check_group(arg, clause) if arg.is_a?(Node) }
270
+ end
271
+ end
272
+ end
273
+ end