Class: BetterAuth::Adapters::SQL

Inherits:
Base
  • Object
show all
Includes:
JoinSupport
Defined in:
lib/better_auth/adapters/sql.rb

Direct Known Subclasses

MSSQL, MySQL, Postgres, SQLite

Constant Summary

Constants inherited from Base

Base::TRANSACTION_CONTEXT_KEY

Instance Attribute Summary collapse

Attributes inherited from Base

#options

Instance Method Summary collapse

Methods inherited from Base

#atomic_transactions?

Constructor Details

#initialize(options, connection:, dialect:) ⇒ SQL

Returns a new instance of SQL.



15
16
17
18
19
20
# File 'lib/better_auth/adapters/sql.rb', line 15

def initialize(options, connection:, dialect:)
  super(options)
  @connection = connection
  @dialect = dialect.to_sym
  @connection_lock = Monitor.new
end

Instance Attribute Details

#connectionObject (readonly)

Returns the value of attribute connection.



13
14
15
# File 'lib/better_auth/adapters/sql.rb', line 13

def connection
  @connection
end

#dialectObject (readonly)

Returns the value of attribute dialect.



13
14
15
# File 'lib/better_auth/adapters/sql.rb', line 13

def dialect
  @dialect
end

Instance Method Details

#consume_one(model:, where:) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/better_auth/adapters/sql.rb', line 164

def consume_one(model:, where:)
  model = model.to_s
  case dialect
  when :postgres, :sqlite
    consume_one_with_returning(model, where)
  when :mssql
    consume_one_with_output(model, where)
  else
    transaction { consume_one_with_lock(model, where) }
  end
end

#count(model:, where: nil) ⇒ Object



153
154
155
156
157
158
159
160
161
162
# File 'lib/better_auth/adapters/sql.rb', line 153

def count(model:, where: nil)
  model = model.to_s
  params = []
  where_sql = build_where(model, where || [], params)
  sql = +"SELECT COUNT(*) AS count FROM "
  sql << quote(table_for(model))
  sql << " WHERE #{where_sql}" unless where_sql.empty?
  row = execute(sql, params).first || {}
  (row["count"] || row[:count] || 0).to_i
end

#create(model:, data:, force_allow_id: false) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/better_auth/adapters/sql.rb', line 22

def create(model:, data:, force_allow_id: false)
  model = model.to_s
  input = transform_input(model, data, "create", force_allow_id)
  table = table_for(model)
  columns = input.keys.map { |field| storage_field(model, field) }
  params = input.keys.map { |field| input[field] }
  placeholders = params.each_index.map { |index| placeholder(index + 1) }
  returning = (dialect == :postgres) ? " RETURNING *" : ""
  sql = "INSERT INTO #{quote(table)} (#{columns.map { |column| quote(column) }.join(", ")}) VALUES (#{placeholders.join(", ")})#{returning}"
  rows = execute(sql, params)
  row = rows.first
  return normalize_record(model, row) if row

  lookup = create_lookup(model, input)
  lookup ? find_one(model: model, where: [lookup]) : input
end

#create_if_absent(model:, data:, conflict_field: "id", force_allow_id: true) ⇒ Object

Raises:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/better_auth/adapters/sql.rb', line 39

def create_if_absent(model:, data:, conflict_field: "id", force_allow_id: true)
  model = model.to_s
  field = atomic_schema_field(schema_for(model).fetch(:fields), conflict_field)
  input = transform_input(model, data, "create", force_allow_id)
  raise APIError.new("BAD_REQUEST", message: "Missing conflict field #{conflict_field}") unless input.key?(field)

  columns = input.keys.map { |key| storage_field(model, key) }
  params = input.values
  values = params.each_index.map { |index| placeholder(index + 1) }.join(", ")
  table = quote(table_for(model))
  conflict_column = quote(storage_field(model, field))
  quoted_columns = columns.map { |column| quote(column) }.join(", ")

  case dialect
  when :postgres, :sqlite
    sql = "INSERT INTO #{table} (#{quoted_columns}) VALUES (#{values}) ON CONFLICT (#{conflict_column}) DO NOTHING RETURNING #{conflict_column}"
    !execute(sql, params).empty?
  when :mysql
    transaction { create_if_absent_mysql(model, field, input, table, quoted_columns, values, params) }
  when :mssql
    sql = "INSERT INTO #{table} (#{quoted_columns}) OUTPUT inserted.#{conflict_column} " \
      "SELECT #{values} WHERE NOT EXISTS (SELECT 1 FROM #{table} WITH (UPDLOCK, HOLDLOCK) WHERE #{conflict_column} = #{placeholder(params.length + 1)})"
    !execute(sql, params + [input.fetch(field)]).empty?
  else
    raise NotImplementedError, "create_if_absent is unsupported for #{dialect}"
  end
end

#delete(model:, where:) ⇒ Object



136
137
138
139
# File 'lib/better_auth/adapters/sql.rb', line 136

def delete(model:, where:)
  delete_many(model: model, where: where)
  nil
end

#delete_many(model:, where:, limit: nil) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/better_auth/adapters/sql.rb', line 141

def delete_many(model:, where:, limit: nil)
  model = model.to_s
  params = []
  where_sql = build_where(model, where || [], params)
  sql = +"DELETE FROM "
  sql << quote(table_for(model))
  sql << " WHERE #{where_sql}" unless where_sql.empty?
  sql << " LIMIT #{Integer(limit)}" if limit && dialect == :mysql
  result = execute(sql, params, affected_rows_result: true)
  affected_rows(result)
end

#find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/better_auth/adapters/sql.rb', line 75

def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
  model = model.to_s
  params = []
  sql = +"SELECT "
  sql << "TOP (#{Integer(limit)}) " if dialect == :mssql && limit && !offset
  sql << select_sql(model, select, join)
  sql << " FROM "
  sql << quote(table_for(model))
  sql << join_sql(model, join)
  where_sql = build_where(model, where || [], params)
  sql << " WHERE #{where_sql}" unless where_sql.empty?
  sql << order_sql(model, sort_by) if sort_by
  append_pagination_sql(sql, model, sort_by, limit, offset)

  records = execute(sql, params).map { |row| normalize_record(model, row, join: join) }
  collection_join?(model, join) ? aggregate_collection_joins(model, records, join) : records
end

#find_one(model:, where: [], select: nil, join: nil) ⇒ Object



67
68
69
70
71
72
73
# File 'lib/better_auth/adapters/sql.rb', line 67

def find_one(model:, where: [], select: nil, join: nil)
  if collection_join?(model.to_s, join)
    find_many(model: model, where: where, select: select, join: join).first
  else
    find_many(model: model, where: where, select: select, join: join, limit: 1).first
  end
end

#increment_one(model:, where:, increment:, set: nil, allow_server_managed: false) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/better_auth/adapters/sql.rb', line 176

def increment_one(model:, where:, increment:, set: nil, allow_server_managed: false)
  model = model.to_s
  increments, assignments = normalize_atomic_update(model, increment, set, allow_server_managed)
  case dialect
  when :postgres, :sqlite
    increment_one_with_returning(model, where, increments, assignments)
  when :mssql
    increment_one_with_output(model, where, increments, assignments)
  else
    transaction { increment_one_with_lock(model, where, increments, assignments) }
  end
end

#transactionObject



189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/better_auth/adapters/sql.rb', line 189

def transaction
  return yield active_transaction_adapter if active_transaction_adapter

  @connection_lock.synchronize do
    execute("BEGIN", [])
    result = with_transaction_context(self) { yield self }
    execute("COMMIT", [])
    result
  rescue
    execute("ROLLBACK", [])
    raise
  end
end

#update(model:, where:, update:) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/better_auth/adapters/sql.rb', line 93

def update(model:, where:, update:)
  model = model.to_s
  return nil if Array(where).empty?

  ensure_update_input_has_fields!(model, update)
  if dialect == :postgres
    records = update_many(model: model, where: where, update: update, returning: true)
    return records.is_a?(Array) ? records.first : records
  end

  existing = find_one(model: model, where: where)
  return nil unless existing

  updated_count = update_many(model: model, where: where, update: update)
  return nil unless updated_count.to_i.positive?

  lookup = record_lookup(model, existing)
  lookup ? find_one(model: model, where: [lookup]) : find_one(model: model, where: where)
end

#update_many(model:, where:, update:, returning: false) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/better_auth/adapters/sql.rb', line 113

def update_many(model:, where:, update:, returning: false)
  model = model.to_s
  ensure_update_input_has_fields!(model, update)
  data = transform_input(model, update, "update", true)
  ensure_update_data!(data)
  params = []
  assignments = data.each_key.map do |field|
    params << data[field]
    "#{quote(storage_field(model, field))} = #{placeholder(params.length)}"
  end
  where_sql = build_where(model, where || [], params)
  sql = +"UPDATE "
  sql << quote(table_for(model))
  sql << " SET "
  sql << assignments.join(", ")
  sql << " WHERE #{where_sql}" unless where_sql.empty?
  sql << " RETURNING *" if dialect == :postgres && returning
  result = execute(sql, params, affected_rows_result: !returning)
  return result.map { |row| normalize_record(model, row) } if returning

  affected_rows(result)
end