Class: Pluckr::Compiler::Sql

Inherits:
Object
  • Object
show all
Defined in:
lib/pluckr/compiler/sql.rb

Overview

Turns a Schema::Scope into SQL.

Everything is built with Arel and ActiveRecord, so PostgreSQL, MySQL and SQLite all get correctly quoted statements from the same code path.

Rules of the MVP:

* plain fields          -> selected columns, never `SELECT users.*`
* singular associations -> LEFT OUTER JOIN + hidden presence marker
* exists                -> correlated EXISTS subquery
* count/sum/min/max     -> scalar subquery (never JOIN + GROUP BY)

The compiler only reads the AST. It never touches DSL state.

Defined Under Namespace

Classes: Build

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema:, source_model: nil) ⇒ Sql

Returns a new instance of Sql.



20
21
22
23
# File 'lib/pluckr/compiler/sql.rb', line 20

def initialize(schema:, source_model: nil)
  @schema = schema
  @source_model = source_model
end

Instance Attribute Details

#schemaObject (readonly)

Returns the value of attribute schema.



18
19
20
# File 'lib/pluckr/compiler/sql.rb', line 18

def schema
  @schema
end

#source_modelObject (readonly)

Returns the value of attribute source_model.



18
19
20
# File 'lib/pluckr/compiler/sql.rb', line 18

def source_model
  @source_model
end

Instance Method Details

#aggregate_only_sqlObject

Single-statement SQL for aggregate-only (dashboard and batch) queries.



38
39
40
41
42
43
44
45
46
# File 'lib/pluckr/compiler/sql.rb', line 38

def aggregate_only_sql
  validate_connections!

  parts = schema.nodes.map.with_index(1) do |node, index|
    "#{scalar_subquery(node, index)} AS #{quote_alias(node.output)}"
  end

  "SELECT #{parts.join(", ")}"
end

#apply(relation) ⇒ Object

Applies Pluckr projections/joins on top of a plain ActiveRecord relation.

Compiled fresh every time: scope: callables may depend on runtime state and the compiler must stay free of shared mutable state.



29
30
31
32
33
34
35
# File 'lib/pluckr/compiler/sql.rb', line 29

def apply(relation)
  validate_connections!
  build = Build.new
  walk(build, schema, [], source_model.arel_table)

  relation.select(*build.projections).joins(build.joins)
end

#connectionObject



48
49
50
# File 'lib/pluckr/compiler/sql.rb', line 48

def connection
  connection_model.connection
end

#validate_connections!Object

A single statement cannot span two databases. Relation-rooted nodes are checked when they are added, in Pluckr::Batch.

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/pluckr/compiler/sql.rb', line 54

def validate_connections!
  models = ([source_model] + independent_models).compact.uniq
  return if models.size < 2

  pool = models.first.connection_pool
  offender = models.find { |model| model.connection_pool != pool }
  return unless offender

  raise ConfigurationError,
        "#{offender.name} and #{models.first.name} use different database connections, " \
        "so they cannot be read in one statement"
end