Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/storage/lisk/LiskDAO.php
12241 views
1
<?php
2
3
/**
4
* Simple object-authoritative data access object that makes it easy to build
5
* stuff that you need to save to a database. Basically, it means that the
6
* amount of boilerplate code (and, particularly, boilerplate SQL) you need
7
* to write is greatly reduced.
8
*
9
* Lisk makes it fairly easy to build something quickly and end up with
10
* reasonably high-quality code when you're done (e.g., getters and setters,
11
* objects, transactions, reasonably structured OO code). It's also very thin:
12
* you can break past it and use MySQL and other lower-level tools when you
13
* need to in those couple of cases where it doesn't handle your workflow
14
* gracefully.
15
*
16
* However, Lisk won't scale past one database and lacks many of the features
17
* of modern DAOs like Hibernate: for instance, it does not support joins or
18
* polymorphic storage.
19
*
20
* This means that Lisk is well-suited for tools like Differential, but often a
21
* poor choice elsewhere. And it is strictly unsuitable for many projects.
22
*
23
* Lisk's model is object-authoritative: the PHP class definition is the
24
* master authority for what the object looks like.
25
*
26
* =Building New Objects=
27
*
28
* To create new Lisk objects, extend @{class:LiskDAO} and implement
29
* @{method:establishLiveConnection}. It should return an
30
* @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your
31
* objects.
32
*
33
* class Dog extends LiskDAO {
34
*
35
* protected $name;
36
* protected $breed;
37
*
38
* public function establishLiveConnection() {
39
* return $some_connection_object;
40
* }
41
* }
42
*
43
* Now, you should create your table:
44
*
45
* lang=sql
46
* CREATE TABLE dog (
47
* id int unsigned not null auto_increment primary key,
48
* name varchar(32) not null,
49
* breed varchar(32) not null,
50
* dateCreated int unsigned not null,
51
* dateModified int unsigned not null
52
* );
53
*
54
* For each property in your class, add a column with the same name to the table
55
* (see @{method:getConfiguration} for information about changing this mapping).
56
* Additionally, you should create the three columns `id`, `dateCreated` and
57
* `dateModified`. Lisk will automatically manage these, using them to implement
58
* autoincrement IDs and timestamps. If you do not want to use these features,
59
* see @{method:getConfiguration} for information on disabling them. At a bare
60
* minimum, you must normally have an `id` column which is a primary or unique
61
* key with a numeric type, although you can change its name by overriding
62
* @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to
63
* return null. Note that many methods rely on a single-part primary key and
64
* will no longer work (they will throw) if you disable it.
65
*
66
* As you add more properties to your class in the future, remember to add them
67
* to the database table as well.
68
*
69
* Lisk will now automatically handle these operations: getting and setting
70
* properties, saving objects, loading individual objects, loading groups
71
* of objects, updating objects, managing IDs, updating timestamps whenever
72
* an object is created or modified, and some additional specialized
73
* operations.
74
*
75
* = Creating, Retrieving, Updating, and Deleting =
76
*
77
* To create and persist a Lisk object, use @{method:save}:
78
*
79
* $dog = id(new Dog())
80
* ->setName('Sawyer')
81
* ->setBreed('Pug')
82
* ->save();
83
*
84
* Note that **Lisk automatically builds getters and setters for all of your
85
* object's protected properties** via @{method:__call}. If you want to add
86
* custom behavior to your getters or setters, you can do so by overriding the
87
* @{method:readField} and @{method:writeField} methods.
88
*
89
* Calling @{method:save} will persist the object to the database. After calling
90
* @{method:save}, you can call @{method:getID} to retrieve the object's ID.
91
*
92
* To load objects by ID, use the @{method:load} method:
93
*
94
* $dog = id(new Dog())->load($id);
95
*
96
* This will load the Dog record with ID $id into $dog, or `null` if no such
97
* record exists (@{method:load} is an instance method rather than a static
98
* method because PHP does not support late static binding, at least until PHP
99
* 5.3).
100
*
101
* To update an object, change its properties and save it:
102
*
103
* $dog->setBreed('Lab')->save();
104
*
105
* To delete an object, call @{method:delete}:
106
*
107
* $dog->delete();
108
*
109
* That's Lisk CRUD in a nutshell.
110
*
111
* = Queries =
112
*
113
* Often, you want to load a bunch of objects, or execute a more specialized
114
* query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:
115
*
116
* $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
117
* $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
118
*
119
* These methods work like @{function@arcanist:queryfx}, but only take half of
120
* a query (the part after the WHERE keyword). Lisk will handle the connection,
121
* columns, and object construction; you are responsible for the rest of it.
122
* @{method:loadAllWhere} returns a list of objects, while
123
* @{method:loadOneWhere} returns a single object (or `null`).
124
*
125
* There's also a @{method:loadRelatives} method which helps to prevent the 1+N
126
* queries problem.
127
*
128
* = Managing Transactions =
129
*
130
* Lisk uses a transaction stack, so code does not generally need to be aware
131
* of the transactional state of objects to implement correct transaction
132
* semantics:
133
*
134
* $obj->openTransaction();
135
* $obj->save();
136
* $other->save();
137
* // ...
138
* $other->openTransaction();
139
* $other->save();
140
* $another->save();
141
* if ($some_condition) {
142
* $other->saveTransaction();
143
* } else {
144
* $other->killTransaction();
145
* }
146
* // ...
147
* $obj->saveTransaction();
148
*
149
* Assuming ##$obj##, ##$other## and ##$another## live on the same database,
150
* this code will work correctly by establishing savepoints.
151
*
152
* Selects whose data are used later in the transaction should be included in
153
* @{method:beginReadLocking} or @{method:beginWriteLocking} block.
154
*
155
* @task conn Managing Connections
156
* @task config Configuring Lisk
157
* @task load Loading Objects
158
* @task info Examining Objects
159
* @task save Writing Objects
160
* @task hook Hooks and Callbacks
161
* @task util Utilities
162
* @task xaction Managing Transactions
163
* @task isolate Isolation for Unit Testing
164
*/
165
abstract class LiskDAO extends Phobject
166
implements AphrontDatabaseTableRefInterface {
167
168
const CONFIG_IDS = 'id-mechanism';
169
const CONFIG_TIMESTAMPS = 'timestamps';
170
const CONFIG_AUX_PHID = 'auxiliary-phid';
171
const CONFIG_SERIALIZATION = 'col-serialization';
172
const CONFIG_BINARY = 'binary';
173
const CONFIG_COLUMN_SCHEMA = 'col-schema';
174
const CONFIG_KEY_SCHEMA = 'key-schema';
175
const CONFIG_NO_TABLE = 'no-table';
176
const CONFIG_NO_MUTATE = 'no-mutate';
177
178
const SERIALIZATION_NONE = 'id';
179
const SERIALIZATION_JSON = 'json';
180
const SERIALIZATION_PHP = 'php';
181
182
const IDS_AUTOINCREMENT = 'ids-auto';
183
const IDS_COUNTER = 'ids-counter';
184
const IDS_MANUAL = 'ids-manual';
185
186
const COUNTER_TABLE_NAME = 'lisk_counter';
187
188
private static $processIsolationLevel = 0;
189
private static $transactionIsolationLevel = 0;
190
191
private $ephemeral = false;
192
private $forcedConnection;
193
194
private static $connections = array();
195
196
private static $liskMetadata = array();
197
198
protected $id;
199
protected $phid;
200
protected $dateCreated;
201
protected $dateModified;
202
203
/**
204
* Build an empty object.
205
*
206
* @return obj Empty object.
207
*/
208
public function __construct() {
209
$id_key = $this->getIDKey();
210
if ($id_key) {
211
$this->$id_key = null;
212
}
213
}
214
215
216
/* -( Managing Connections )----------------------------------------------- */
217
218
219
/**
220
* Establish a live connection to a database service. This method should
221
* return a new connection. Lisk handles connection caching and management;
222
* do not perform caching deeper in the stack.
223
*
224
* @param string Mode, either 'r' (reading) or 'w' (reading and writing).
225
* @return AphrontDatabaseConnection New database connection.
226
* @task conn
227
*/
228
abstract protected function establishLiveConnection($mode);
229
230
231
/**
232
* Return a namespace for this object's connections in the connection cache.
233
* Generally, the database name is appropriate. Two connections are considered
234
* equivalent if they have the same connection namespace and mode.
235
*
236
* @return string Connection namespace for cache
237
* @task conn
238
*/
239
protected function getConnectionNamespace() {
240
return $this->getDatabaseName();
241
}
242
243
abstract protected function getDatabaseName();
244
245
/**
246
* Get an existing, cached connection for this object.
247
*
248
* @param mode Connection mode.
249
* @return AphrontDatabaseConnection|null Connection, if it exists in cache.
250
* @task conn
251
*/
252
protected function getEstablishedConnection($mode) {
253
$key = $this->getConnectionNamespace().':'.$mode;
254
if (isset(self::$connections[$key])) {
255
return self::$connections[$key];
256
}
257
return null;
258
}
259
260
261
/**
262
* Store a connection in the connection cache.
263
*
264
* @param mode Connection mode.
265
* @param AphrontDatabaseConnection Connection to cache.
266
* @return this
267
* @task conn
268
*/
269
protected function setEstablishedConnection(
270
$mode,
271
AphrontDatabaseConnection $connection,
272
$force_unique = false) {
273
274
$key = $this->getConnectionNamespace().':'.$mode;
275
276
if ($force_unique) {
277
$key .= ':unique';
278
while (isset(self::$connections[$key])) {
279
$key .= '!';
280
}
281
}
282
283
self::$connections[$key] = $connection;
284
return $this;
285
}
286
287
288
/**
289
* Force an object to use a specific connection.
290
*
291
* This overrides all connection management and forces the object to use
292
* a specific connection when interacting with the database.
293
*
294
* @param AphrontDatabaseConnection Connection to force this object to use.
295
* @task conn
296
*/
297
public function setForcedConnection(AphrontDatabaseConnection $connection) {
298
$this->forcedConnection = $connection;
299
return $this;
300
}
301
302
303
/* -( Configuring Lisk )--------------------------------------------------- */
304
305
306
/**
307
* Change Lisk behaviors, like ID configuration and timestamps. If you want
308
* to change these behaviors, you should override this method in your child
309
* class and change the options you're interested in. For example:
310
*
311
* protected function getConfiguration() {
312
* return array(
313
* Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
314
* ) + parent::getConfiguration();
315
* }
316
*
317
* The available options are:
318
*
319
* CONFIG_IDS
320
* Lisk objects need to have a unique identifying ID. The three mechanisms
321
* available for generating this ID are IDS_AUTOINCREMENT (default, assumes
322
* the ID column is an autoincrement primary key), IDS_MANUAL (you are taking
323
* full responsibility for ID management), or IDS_COUNTER (see below).
324
*
325
* InnoDB does not persist the value of `auto_increment` across restarts,
326
* and instead initializes it to `MAX(id) + 1` during startup. This means it
327
* may reissue the same autoincrement ID more than once, if the row is deleted
328
* and then the database is restarted. To avoid this, you can set an object to
329
* use a counter table with IDS_COUNTER. This will generally behave like
330
* IDS_AUTOINCREMENT, except that the counter value will persist across
331
* restarts and inserts will be slightly slower. If a database stores any
332
* DAOs which use this mechanism, you must create a table there with this
333
* schema:
334
*
335
* CREATE TABLE lisk_counter (
336
* counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY,
337
* counterValue BIGINT UNSIGNED NOT NULL
338
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
339
*
340
* CONFIG_TIMESTAMPS
341
* Lisk can automatically handle keeping track of a `dateCreated' and
342
* `dateModified' column, which it will update when it creates or modifies
343
* an object. If you don't want to do this, you may disable this option.
344
* By default, this option is ON.
345
*
346
* CONFIG_AUX_PHID
347
* This option can be enabled by being set to some truthy value. The meaning
348
* of this value is defined by your PHID generation mechanism. If this option
349
* is enabled, a `phid' property will be populated with a unique PHID when an
350
* object is created (or if it is saved and does not currently have one). You
351
* need to override generatePHID() and hook it into your PHID generation
352
* mechanism for this to work. By default, this option is OFF.
353
*
354
* CONFIG_SERIALIZATION
355
* You can optionally provide a column serialization map that will be applied
356
* to values when they are written to the database. For example:
357
*
358
* self::CONFIG_SERIALIZATION => array(
359
* 'complex' => self::SERIALIZATION_JSON,
360
* )
361
*
362
* This will cause Lisk to JSON-serialize the 'complex' field before it is
363
* written, and unserialize it when it is read.
364
*
365
* CONFIG_BINARY
366
* You can optionally provide a map of columns to a flag indicating that
367
* they store binary data. These columns will not raise an error when
368
* handling binary writes.
369
*
370
* CONFIG_COLUMN_SCHEMA
371
* Provide a map of columns to schema column types.
372
*
373
* CONFIG_KEY_SCHEMA
374
* Provide a map of key names to key specifications.
375
*
376
* CONFIG_NO_TABLE
377
* Allows you to specify that this object does not actually have a table in
378
* the database.
379
*
380
* CONFIG_NO_MUTATE
381
* Provide a map of columns which should not be included in UPDATE statements.
382
* If you have some columns which are always written to explicitly and should
383
* never be overwritten by a save(), you can specify them here. This is an
384
* advanced, specialized feature and there are usually better approaches for
385
* most locking/contention problems.
386
*
387
* @return dictionary Map of configuration options to values.
388
*
389
* @task config
390
*/
391
protected function getConfiguration() {
392
return array(
393
self::CONFIG_IDS => self::IDS_AUTOINCREMENT,
394
self::CONFIG_TIMESTAMPS => true,
395
);
396
}
397
398
399
/**
400
* Determine the setting of a configuration option for this class of objects.
401
*
402
* @param const Option name, one of the CONFIG_* constants.
403
* @return mixed Option value, if configured (null if unavailable).
404
*
405
* @task config
406
*/
407
public function getConfigOption($option_name) {
408
$options = $this->getLiskMetadata('config');
409
410
if ($options === null) {
411
$options = $this->getConfiguration();
412
$this->setLiskMetadata('config', $options);
413
}
414
415
return idx($options, $option_name);
416
}
417
418
419
/* -( Loading Objects )---------------------------------------------------- */
420
421
422
/**
423
* Load an object by ID. You need to invoke this as an instance method, not
424
* a class method, because PHP doesn't have late static binding (until
425
* PHP 5.3.0). For example:
426
*
427
* $dog = id(new Dog())->load($dog_id);
428
*
429
* @param int Numeric ID identifying the object to load.
430
* @return obj|null Identified object, or null if it does not exist.
431
*
432
* @task load
433
*/
434
public function load($id) {
435
if (is_object($id)) {
436
$id = (string)$id;
437
}
438
439
if (!$id || (!is_int($id) && !ctype_digit($id))) {
440
return null;
441
}
442
443
return $this->loadOneWhere(
444
'%C = %d',
445
$this->getIDKey(),
446
$id);
447
}
448
449
450
/**
451
* Loads all of the objects, unconditionally.
452
*
453
* @return dict Dictionary of all persisted objects of this type, keyed
454
* on object ID.
455
*
456
* @task load
457
*/
458
public function loadAll() {
459
return $this->loadAllWhere('1 = 1');
460
}
461
462
463
/**
464
* Load all objects which match a WHERE clause. You provide everything after
465
* the 'WHERE'; Lisk handles everything up to it. For example:
466
*
467
* $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
468
*
469
* The pattern and arguments are as per queryfx().
470
*
471
* @param string queryfx()-style SQL WHERE clause.
472
* @param ... Zero or more conversions.
473
* @return dict Dictionary of matching objects, keyed on ID.
474
*
475
* @task load
476
*/
477
public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) {
478
$args = func_get_args();
479
$data = call_user_func_array(
480
array($this, 'loadRawDataWhere'),
481
$args);
482
return $this->loadAllFromArray($data);
483
}
484
485
486
/**
487
* Load a single object identified by a 'WHERE' clause. You provide
488
* everything after the 'WHERE', and Lisk builds the first half of the
489
* query. See loadAllWhere(). This method is similar, but returns a single
490
* result instead of a list.
491
*
492
* @param string queryfx()-style SQL WHERE clause.
493
* @param ... Zero or more conversions.
494
* @return obj|null Matching object, or null if no object matches.
495
*
496
* @task load
497
*/
498
public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) {
499
$args = func_get_args();
500
$data = call_user_func_array(
501
array($this, 'loadRawDataWhere'),
502
$args);
503
504
if (count($data) > 1) {
505
throw new AphrontCountQueryException(
506
pht(
507
'More than one result from %s!',
508
__FUNCTION__.'()'));
509
}
510
511
$data = reset($data);
512
if (!$data) {
513
return null;
514
}
515
516
return $this->loadFromArray($data);
517
}
518
519
520
protected function loadRawDataWhere($pattern /* , $args... */) {
521
$conn = $this->establishConnection('r');
522
523
if ($conn->isReadLocking()) {
524
$lock_clause = qsprintf($conn, 'FOR UPDATE');
525
} else if ($conn->isWriteLocking()) {
526
$lock_clause = qsprintf($conn, 'LOCK IN SHARE MODE');
527
} else {
528
$lock_clause = qsprintf($conn, '');
529
}
530
531
$args = func_get_args();
532
$args = array_slice($args, 1);
533
534
$pattern = 'SELECT * FROM %R WHERE '.$pattern.' %Q';
535
array_unshift($args, $this);
536
array_push($args, $lock_clause);
537
array_unshift($args, $pattern);
538
539
return call_user_func_array(array($conn, 'queryData'), $args);
540
}
541
542
543
/**
544
* Reload an object from the database, discarding any changes to persistent
545
* properties. This is primarily useful after entering a transaction but
546
* before applying changes to an object.
547
*
548
* @return this
549
*
550
* @task load
551
*/
552
public function reload() {
553
if (!$this->getID()) {
554
throw new Exception(
555
pht("Unable to reload object that hasn't been loaded!"));
556
}
557
558
$result = $this->loadOneWhere(
559
'%C = %d',
560
$this->getIDKey(),
561
$this->getID());
562
563
if (!$result) {
564
throw new AphrontObjectMissingQueryException();
565
}
566
567
return $this;
568
}
569
570
571
/**
572
* Initialize this object's properties from a dictionary. Generally, you
573
* load single objects with loadOneWhere(), but sometimes it may be more
574
* convenient to pull data from elsewhere directly (e.g., a complicated
575
* join via @{method:queryData}) and then load from an array representation.
576
*
577
* @param dict Dictionary of properties, which should be equivalent to
578
* selecting a row from the table or calling
579
* @{method:getProperties}.
580
* @return this
581
*
582
* @task load
583
*/
584
public function loadFromArray(array $row) {
585
$valid_map = $this->getLiskMetadata('validMap', array());
586
587
$map = array();
588
$updated = false;
589
foreach ($row as $k => $v) {
590
// We permit (but ignore) extra properties in the array because a
591
// common approach to building the array is to issue a raw SELECT query
592
// which may include extra explicit columns or joins.
593
594
// This pathway is very hot on some pages, so we're inlining a cache
595
// and doing some microoptimization to avoid a strtolower() call for each
596
// assignment. The common path (assigning a valid property which we've
597
// already seen) always incurs only one empty(). The second most common
598
// path (assigning an invalid property which we've already seen) costs
599
// an empty() plus an isset().
600
601
if (empty($valid_map[$k])) {
602
if (isset($valid_map[$k])) {
603
// The value is set but empty, which means it's false, so we've
604
// already determined it's not valid. We don't need to check again.
605
continue;
606
}
607
$valid_map[$k] = $this->hasProperty($k);
608
$updated = true;
609
if (!$valid_map[$k]) {
610
continue;
611
}
612
}
613
614
$map[$k] = $v;
615
}
616
617
if ($updated) {
618
$this->setLiskMetadata('validMap', $valid_map);
619
}
620
621
$this->willReadData($map);
622
623
foreach ($map as $prop => $value) {
624
$this->$prop = $value;
625
}
626
627
$this->didReadData();
628
629
return $this;
630
}
631
632
633
/**
634
* Initialize a list of objects from a list of dictionaries. Usually you
635
* load lists of objects with @{method:loadAllWhere}, but sometimes that
636
* isn't flexible enough. One case is if you need to do joins to select the
637
* right objects:
638
*
639
* function loadAllWithOwner($owner) {
640
* $data = $this->queryData(
641
* 'SELECT d.*
642
* FROM owner o
643
* JOIN owner_has_dog od ON o.id = od.ownerID
644
* JOIN dog d ON od.dogID = d.id
645
* WHERE o.id = %d',
646
* $owner);
647
* return $this->loadAllFromArray($data);
648
* }
649
*
650
* This is a lot messier than @{method:loadAllWhere}, but more flexible.
651
*
652
* @param list List of property dictionaries.
653
* @return dict List of constructed objects, keyed on ID.
654
*
655
* @task load
656
*/
657
public function loadAllFromArray(array $rows) {
658
$result = array();
659
660
$id_key = $this->getIDKey();
661
662
foreach ($rows as $row) {
663
$obj = clone $this;
664
if ($id_key && isset($row[$id_key])) {
665
$row_id = $row[$id_key];
666
667
if (isset($result[$row_id])) {
668
throw new Exception(
669
pht(
670
'Rows passed to "loadAllFromArray(...)" include two or more '.
671
'rows with the same ID ("%s"). Rows must have unique IDs. '.
672
'An underlying query may be missing a GROUP BY.',
673
$row_id));
674
}
675
676
$result[$row_id] = $obj->loadFromArray($row);
677
} else {
678
$result[] = $obj->loadFromArray($row);
679
}
680
}
681
682
return $result;
683
}
684
685
686
/* -( Examining Objects )-------------------------------------------------- */
687
688
689
/**
690
* Set unique ID identifying this object. You normally don't need to call this
691
* method unless with `IDS_MANUAL`.
692
*
693
* @param mixed Unique ID.
694
* @return this
695
* @task save
696
*/
697
public function setID($id) {
698
$id_key = $this->getIDKey();
699
$this->$id_key = $id;
700
return $this;
701
}
702
703
704
/**
705
* Retrieve the unique ID identifying this object. This value will be null if
706
* the object hasn't been persisted and you didn't set it manually.
707
*
708
* @return mixed Unique ID.
709
*
710
* @task info
711
*/
712
public function getID() {
713
$id_key = $this->getIDKey();
714
return $this->$id_key;
715
}
716
717
718
public function getPHID() {
719
return $this->phid;
720
}
721
722
723
/**
724
* Test if a property exists.
725
*
726
* @param string Property name.
727
* @return bool True if the property exists.
728
* @task info
729
*/
730
public function hasProperty($property) {
731
return (bool)$this->checkProperty($property);
732
}
733
734
735
/**
736
* Retrieve a list of all object properties. This list only includes
737
* properties that are declared as protected, and it is expected that
738
* all properties returned by this function should be persisted to the
739
* database.
740
* Properties that should not be persisted must be declared as private.
741
*
742
* @return dict Dictionary of normalized (lowercase) to canonical (original
743
* case) property names.
744
*
745
* @task info
746
*/
747
protected function getAllLiskProperties() {
748
$properties = $this->getLiskMetadata('properties');
749
750
if ($properties === null) {
751
$class = new ReflectionClass(static::class);
752
$properties = array();
753
foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
754
$properties[strtolower($p->getName())] = $p->getName();
755
}
756
757
$id_key = $this->getIDKey();
758
if ($id_key != 'id') {
759
unset($properties['id']);
760
}
761
762
if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
763
unset($properties['datecreated']);
764
unset($properties['datemodified']);
765
}
766
767
if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {
768
unset($properties['phid']);
769
}
770
771
$this->setLiskMetadata('properties', $properties);
772
}
773
774
return $properties;
775
}
776
777
778
/**
779
* Check if a property exists on this object.
780
*
781
* @return string|null Canonical property name, or null if the property
782
* does not exist.
783
*
784
* @task info
785
*/
786
protected function checkProperty($property) {
787
$properties = $this->getAllLiskProperties();
788
789
$property = strtolower($property);
790
if (empty($properties[$property])) {
791
return null;
792
}
793
794
return $properties[$property];
795
}
796
797
798
/**
799
* Get or build the database connection for this object.
800
*
801
* @param string 'r' for read, 'w' for read/write.
802
* @param bool True to force a new connection. The connection will not
803
* be retrieved from or saved into the connection cache.
804
* @return AphrontDatabaseConnection Lisk connection object.
805
*
806
* @task info
807
*/
808
public function establishConnection($mode, $force_new = false) {
809
if ($mode != 'r' && $mode != 'w') {
810
throw new Exception(
811
pht(
812
"Unknown mode '%s', should be 'r' or 'w'.",
813
$mode));
814
}
815
816
if ($this->forcedConnection) {
817
return $this->forcedConnection;
818
}
819
820
if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) {
821
$mode = 'isolate-'.$mode;
822
823
$connection = $this->getEstablishedConnection($mode);
824
if (!$connection) {
825
$connection = $this->establishIsolatedConnection($mode);
826
$this->setEstablishedConnection($mode, $connection);
827
}
828
829
return $connection;
830
}
831
832
if (self::shouldIsolateAllLiskEffectsToTransactions()) {
833
// If we're doing fixture transaction isolation, force the mode to 'w'
834
// so we always get the same connection for reads and writes, and thus
835
// can see the writes inside the transaction.
836
$mode = 'w';
837
}
838
839
// TODO: There is currently no protection on 'r' queries against writing.
840
841
$connection = null;
842
if (!$force_new) {
843
if ($mode == 'r') {
844
// If we're requesting a read connection but already have a write
845
// connection, reuse the write connection so that reads can take place
846
// inside transactions.
847
$connection = $this->getEstablishedConnection('w');
848
}
849
850
if (!$connection) {
851
$connection = $this->getEstablishedConnection($mode);
852
}
853
}
854
855
if (!$connection) {
856
$connection = $this->establishLiveConnection($mode);
857
if (self::shouldIsolateAllLiskEffectsToTransactions()) {
858
$connection->openTransaction();
859
}
860
$this->setEstablishedConnection(
861
$mode,
862
$connection,
863
$force_unique = $force_new);
864
}
865
866
return $connection;
867
}
868
869
870
/**
871
* Convert this object into a property dictionary. This dictionary can be
872
* restored into an object by using @{method:loadFromArray} (unless you're
873
* using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you
874
* should just go ahead and die in a fire).
875
*
876
* @return dict Dictionary of object properties.
877
*
878
* @task info
879
*/
880
protected function getAllLiskPropertyValues() {
881
$map = array();
882
foreach ($this->getAllLiskProperties() as $p) {
883
// We may receive a warning here for properties we've implicitly added
884
// through configuration; squelch it.
885
$map[$p] = @$this->$p;
886
}
887
return $map;
888
}
889
890
891
/* -( Writing Objects )---------------------------------------------------- */
892
893
894
/**
895
* Make an object read-only.
896
*
897
* Making an object ephemeral indicates that you will be changing state in
898
* such a way that you would never ever want it to be written back to the
899
* storage.
900
*/
901
public function makeEphemeral() {
902
$this->ephemeral = true;
903
return $this;
904
}
905
906
private function isEphemeralCheck() {
907
if ($this->ephemeral) {
908
throw new LiskEphemeralObjectException();
909
}
910
}
911
912
/**
913
* Persist this object to the database. In most cases, this is the only
914
* method you need to call to do writes. If the object has not yet been
915
* inserted this will do an insert; if it has, it will do an update.
916
*
917
* @return this
918
*
919
* @task save
920
*/
921
public function save() {
922
if ($this->shouldInsertWhenSaved()) {
923
return $this->insert();
924
} else {
925
return $this->update();
926
}
927
}
928
929
930
/**
931
* Save this object, forcing the query to use REPLACE regardless of object
932
* state.
933
*
934
* @return this
935
*
936
* @task save
937
*/
938
public function replace() {
939
$this->isEphemeralCheck();
940
return $this->insertRecordIntoDatabase('REPLACE');
941
}
942
943
944
/**
945
* Save this object, forcing the query to use INSERT regardless of object
946
* state.
947
*
948
* @return this
949
*
950
* @task save
951
*/
952
public function insert() {
953
$this->isEphemeralCheck();
954
return $this->insertRecordIntoDatabase('INSERT');
955
}
956
957
958
/**
959
* Save this object, forcing the query to use UPDATE regardless of object
960
* state.
961
*
962
* @return this
963
*
964
* @task save
965
*/
966
public function update() {
967
$this->isEphemeralCheck();
968
969
$this->willSaveObject();
970
$data = $this->getAllLiskPropertyValues();
971
972
// Remove columns flagged as nonmutable from the update statement.
973
$no_mutate = $this->getConfigOption(self::CONFIG_NO_MUTATE);
974
if ($no_mutate) {
975
foreach ($no_mutate as $column) {
976
unset($data[$column]);
977
}
978
}
979
980
$this->willWriteData($data);
981
982
$map = array();
983
foreach ($data as $k => $v) {
984
$map[$k] = $v;
985
}
986
987
$conn = $this->establishConnection('w');
988
$binary = $this->getBinaryColumns();
989
990
foreach ($map as $key => $value) {
991
if (!empty($binary[$key])) {
992
$map[$key] = qsprintf($conn, '%C = %nB', $key, $value);
993
} else {
994
$map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
995
}
996
}
997
998
$id = $this->getID();
999
$conn->query(
1000
'UPDATE %R SET %LQ WHERE %C = '.(is_int($id) ? '%d' : '%s'),
1001
$this,
1002
$map,
1003
$this->getIDKey(),
1004
$id);
1005
// We can't detect a missing object because updating an object without
1006
// changing any values doesn't affect rows. We could jiggle timestamps
1007
// to catch this for objects which track them if we wanted.
1008
1009
$this->didWriteData();
1010
1011
return $this;
1012
}
1013
1014
1015
/**
1016
* Delete this object, permanently.
1017
*
1018
* @return this
1019
*
1020
* @task save
1021
*/
1022
public function delete() {
1023
$this->isEphemeralCheck();
1024
$this->willDelete();
1025
1026
$conn = $this->establishConnection('w');
1027
$conn->query(
1028
'DELETE FROM %R WHERE %C = %d',
1029
$this,
1030
$this->getIDKey(),
1031
$this->getID());
1032
1033
$this->didDelete();
1034
1035
return $this;
1036
}
1037
1038
/**
1039
* Internal implementation of INSERT and REPLACE.
1040
*
1041
* @param const Either "INSERT" or "REPLACE", to force the desired mode.
1042
* @return this
1043
*
1044
* @task save
1045
*/
1046
protected function insertRecordIntoDatabase($mode) {
1047
$this->willSaveObject();
1048
$data = $this->getAllLiskPropertyValues();
1049
1050
$conn = $this->establishConnection('w');
1051
1052
$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
1053
switch ($id_mechanism) {
1054
case self::IDS_AUTOINCREMENT:
1055
// If we are using autoincrement IDs, let MySQL assign the value for the
1056
// ID column, if it is empty. If the caller has explicitly provided a
1057
// value, use it.
1058
$id_key = $this->getIDKey();
1059
if (empty($data[$id_key])) {
1060
unset($data[$id_key]);
1061
}
1062
break;
1063
case self::IDS_COUNTER:
1064
// If we are using counter IDs, assign a new ID if we don't already have
1065
// one.
1066
$id_key = $this->getIDKey();
1067
if (empty($data[$id_key])) {
1068
$counter_name = $this->getTableName();
1069
$id = self::loadNextCounterValue($conn, $counter_name);
1070
$this->setID($id);
1071
$data[$id_key] = $id;
1072
}
1073
break;
1074
case self::IDS_MANUAL:
1075
break;
1076
default:
1077
throw new Exception(pht('Unknown %s mechanism!', 'CONFIG_IDs'));
1078
}
1079
1080
$this->willWriteData($data);
1081
1082
$columns = array_keys($data);
1083
$binary = $this->getBinaryColumns();
1084
1085
foreach ($data as $key => $value) {
1086
try {
1087
if (!empty($binary[$key])) {
1088
$data[$key] = qsprintf($conn, '%nB', $value);
1089
} else {
1090
$data[$key] = qsprintf($conn, '%ns', $value);
1091
}
1092
} catch (AphrontParameterQueryException $parameter_exception) {
1093
throw new PhutilProxyException(
1094
pht(
1095
"Unable to insert or update object of class %s, field '%s' ".
1096
"has a non-scalar value.",
1097
get_class($this),
1098
$key),
1099
$parameter_exception);
1100
}
1101
}
1102
1103
switch ($mode) {
1104
case 'INSERT':
1105
$verb = qsprintf($conn, 'INSERT');
1106
break;
1107
case 'REPLACE':
1108
$verb = qsprintf($conn, 'REPLACE');
1109
break;
1110
default:
1111
throw new Exception(
1112
pht(
1113
'Insert mode verb "%s" is not recognized, use INSERT or REPLACE.',
1114
$mode));
1115
}
1116
1117
$conn->query(
1118
'%Q INTO %R (%LC) VALUES (%LQ)',
1119
$verb,
1120
$this,
1121
$columns,
1122
$data);
1123
1124
// Only use the insert id if this table is using auto-increment ids
1125
if ($id_mechanism === self::IDS_AUTOINCREMENT) {
1126
$this->setID($conn->getInsertID());
1127
}
1128
1129
$this->didWriteData();
1130
1131
return $this;
1132
}
1133
1134
1135
/**
1136
* Method used to determine whether to insert or update when saving.
1137
*
1138
* @return bool true if the record should be inserted
1139
*/
1140
protected function shouldInsertWhenSaved() {
1141
$key_type = $this->getConfigOption(self::CONFIG_IDS);
1142
1143
if ($key_type == self::IDS_MANUAL) {
1144
throw new Exception(
1145
pht(
1146
'You are using manual IDs. You must override the %s method '.
1147
'to properly detect when to insert a new record.',
1148
__FUNCTION__.'()'));
1149
} else {
1150
return !$this->getID();
1151
}
1152
}
1153
1154
1155
/* -( Hooks and Callbacks )------------------------------------------------ */
1156
1157
1158
/**
1159
* Retrieve the database table name. By default, this is the class name.
1160
*
1161
* @return string Table name for object storage.
1162
*
1163
* @task hook
1164
*/
1165
public function getTableName() {
1166
return get_class($this);
1167
}
1168
1169
1170
/**
1171
* Retrieve the primary key column, "id" by default. If you can not
1172
* reasonably name your ID column "id", override this method.
1173
*
1174
* @return string Name of the ID column.
1175
*
1176
* @task hook
1177
*/
1178
public function getIDKey() {
1179
return 'id';
1180
}
1181
1182
/**
1183
* Generate a new PHID, used by CONFIG_AUX_PHID.
1184
*
1185
* @return phid Unique, newly allocated PHID.
1186
*
1187
* @task hook
1188
*/
1189
public function generatePHID() {
1190
$type = $this->getPHIDType();
1191
return PhabricatorPHID::generateNewPHID($type);
1192
}
1193
1194
public function getPHIDType() {
1195
throw new PhutilMethodNotImplementedException();
1196
}
1197
1198
1199
/**
1200
* Hook to apply serialization or validation to data before it is written to
1201
* the database. See also @{method:willReadData}.
1202
*
1203
* @task hook
1204
*/
1205
protected function willWriteData(array &$data) {
1206
$this->applyLiskDataSerialization($data, false);
1207
}
1208
1209
1210
/**
1211
* Hook to perform actions after data has been written to the database.
1212
*
1213
* @task hook
1214
*/
1215
protected function didWriteData() {}
1216
1217
1218
/**
1219
* Hook to make internal object state changes prior to INSERT, REPLACE or
1220
* UPDATE.
1221
*
1222
* @task hook
1223
*/
1224
protected function willSaveObject() {
1225
$use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
1226
1227
if ($use_timestamps) {
1228
if (!$this->getDateCreated()) {
1229
$this->setDateCreated(time());
1230
}
1231
$this->setDateModified(time());
1232
}
1233
1234
if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) {
1235
$this->setPHID($this->generatePHID());
1236
}
1237
}
1238
1239
1240
/**
1241
* Hook to apply serialization or validation to data as it is read from the
1242
* database. See also @{method:willWriteData}.
1243
*
1244
* @task hook
1245
*/
1246
protected function willReadData(array &$data) {
1247
$this->applyLiskDataSerialization($data, $deserialize = true);
1248
}
1249
1250
/**
1251
* Hook to perform an action on data after it is read from the database.
1252
*
1253
* @task hook
1254
*/
1255
protected function didReadData() {}
1256
1257
/**
1258
* Hook to perform an action before the deletion of an object.
1259
*
1260
* @task hook
1261
*/
1262
protected function willDelete() {}
1263
1264
/**
1265
* Hook to perform an action after the deletion of an object.
1266
*
1267
* @task hook
1268
*/
1269
protected function didDelete() {}
1270
1271
/**
1272
* Reads the value from a field. Override this method for custom behavior
1273
* of @{method:getField} instead of overriding getField directly.
1274
*
1275
* @param string Canonical field name
1276
* @return mixed Value of the field
1277
*
1278
* @task hook
1279
*/
1280
protected function readField($field) {
1281
if (isset($this->$field)) {
1282
return $this->$field;
1283
}
1284
return null;
1285
}
1286
1287
/**
1288
* Writes a value to a field. Override this method for custom behavior of
1289
* setField($value) instead of overriding setField directly.
1290
*
1291
* @param string Canonical field name
1292
* @param mixed Value to write
1293
*
1294
* @task hook
1295
*/
1296
protected function writeField($field, $value) {
1297
$this->$field = $value;
1298
}
1299
1300
1301
/* -( Manging Transactions )----------------------------------------------- */
1302
1303
1304
/**
1305
* Increase transaction stack depth.
1306
*
1307
* @return this
1308
*/
1309
public function openTransaction() {
1310
$this->establishConnection('w')->openTransaction();
1311
return $this;
1312
}
1313
1314
1315
/**
1316
* Decrease transaction stack depth, saving work.
1317
*
1318
* @return this
1319
*/
1320
public function saveTransaction() {
1321
$this->establishConnection('w')->saveTransaction();
1322
return $this;
1323
}
1324
1325
1326
/**
1327
* Decrease transaction stack depth, discarding work.
1328
*
1329
* @return this
1330
*/
1331
public function killTransaction() {
1332
$this->establishConnection('w')->killTransaction();
1333
return $this;
1334
}
1335
1336
1337
/**
1338
* Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
1339
* other connections can not read them (this is an enormous oversimplification
1340
* of FOR UPDATE semantics; consult the MySQL documentation for details). To
1341
* end read locking, call @{method:endReadLocking}. For example:
1342
*
1343
* $beach->openTransaction();
1344
* $beach->beginReadLocking();
1345
*
1346
* $beach->reload();
1347
* $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
1348
* $beach->save();
1349
*
1350
* $beach->endReadLocking();
1351
* $beach->saveTransaction();
1352
*
1353
* @return this
1354
* @task xaction
1355
*/
1356
public function beginReadLocking() {
1357
$this->establishConnection('w')->beginReadLocking();
1358
return $this;
1359
}
1360
1361
1362
/**
1363
* Ends read-locking that began at an earlier @{method:beginReadLocking} call.
1364
*
1365
* @return this
1366
* @task xaction
1367
*/
1368
public function endReadLocking() {
1369
$this->establishConnection('w')->endReadLocking();
1370
return $this;
1371
}
1372
1373
/**
1374
* Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
1375
* that other connections can not update or delete them (this is an
1376
* oversimplification of LOCK IN SHARE MODE semantics; consult the
1377
* MySQL documentation for details). To end write locking, call
1378
* @{method:endWriteLocking}.
1379
*
1380
* @return this
1381
* @task xaction
1382
*/
1383
public function beginWriteLocking() {
1384
$this->establishConnection('w')->beginWriteLocking();
1385
return $this;
1386
}
1387
1388
1389
/**
1390
* Ends write-locking that began at an earlier @{method:beginWriteLocking}
1391
* call.
1392
*
1393
* @return this
1394
* @task xaction
1395
*/
1396
public function endWriteLocking() {
1397
$this->establishConnection('w')->endWriteLocking();
1398
return $this;
1399
}
1400
1401
1402
/* -( Isolation )---------------------------------------------------------- */
1403
1404
1405
/**
1406
* @task isolate
1407
*/
1408
public static function beginIsolateAllLiskEffectsToCurrentProcess() {
1409
self::$processIsolationLevel++;
1410
}
1411
1412
/**
1413
* @task isolate
1414
*/
1415
public static function endIsolateAllLiskEffectsToCurrentProcess() {
1416
self::$processIsolationLevel--;
1417
if (self::$processIsolationLevel < 0) {
1418
throw new Exception(
1419
pht('Lisk process isolation level was reduced below 0.'));
1420
}
1421
}
1422
1423
/**
1424
* @task isolate
1425
*/
1426
public static function shouldIsolateAllLiskEffectsToCurrentProcess() {
1427
return (bool)self::$processIsolationLevel;
1428
}
1429
1430
/**
1431
* @task isolate
1432
*/
1433
private function establishIsolatedConnection($mode) {
1434
$config = array();
1435
return new AphrontIsolatedDatabaseConnection($config);
1436
}
1437
1438
/**
1439
* @task isolate
1440
*/
1441
public static function beginIsolateAllLiskEffectsToTransactions() {
1442
if (self::$transactionIsolationLevel === 0) {
1443
self::closeAllConnections();
1444
}
1445
self::$transactionIsolationLevel++;
1446
}
1447
1448
/**
1449
* @task isolate
1450
*/
1451
public static function endIsolateAllLiskEffectsToTransactions() {
1452
self::$transactionIsolationLevel--;
1453
if (self::$transactionIsolationLevel < 0) {
1454
throw new Exception(
1455
pht('Lisk transaction isolation level was reduced below 0.'));
1456
} else if (self::$transactionIsolationLevel == 0) {
1457
foreach (self::$connections as $key => $conn) {
1458
if ($conn) {
1459
$conn->killTransaction();
1460
}
1461
}
1462
self::closeAllConnections();
1463
}
1464
}
1465
1466
/**
1467
* @task isolate
1468
*/
1469
public static function shouldIsolateAllLiskEffectsToTransactions() {
1470
return (bool)self::$transactionIsolationLevel;
1471
}
1472
1473
/**
1474
* Close any connections with no recent activity.
1475
*
1476
* Long-running processes can use this method to clean up connections which
1477
* have not been used recently.
1478
*
1479
* @param int Close connections with no activity for this many seconds.
1480
* @return void
1481
*/
1482
public static function closeInactiveConnections($idle_window) {
1483
$connections = self::$connections;
1484
1485
$now = PhabricatorTime::getNow();
1486
foreach ($connections as $key => $connection) {
1487
// If the connection is not idle, never consider it inactive.
1488
if (!$connection->isIdle()) {
1489
continue;
1490
}
1491
1492
$last_active = $connection->getLastActiveEpoch();
1493
1494
$idle_duration = ($now - $last_active);
1495
if ($idle_duration <= $idle_window) {
1496
continue;
1497
}
1498
1499
self::closeConnection($key);
1500
}
1501
}
1502
1503
1504
public static function closeAllConnections() {
1505
$connections = self::$connections;
1506
1507
foreach ($connections as $key => $connection) {
1508
self::closeConnection($key);
1509
}
1510
}
1511
1512
public static function closeIdleConnections() {
1513
$connections = self::$connections;
1514
1515
foreach ($connections as $key => $connection) {
1516
if (!$connection->isIdle()) {
1517
continue;
1518
}
1519
1520
self::closeConnection($key);
1521
}
1522
}
1523
1524
private static function closeConnection($key) {
1525
if (empty(self::$connections[$key])) {
1526
throw new Exception(
1527
pht(
1528
'No database connection with connection key "%s" exists!',
1529
$key));
1530
}
1531
1532
$connection = self::$connections[$key];
1533
unset(self::$connections[$key]);
1534
1535
$connection->close();
1536
}
1537
1538
1539
/* -( Utilities )---------------------------------------------------------- */
1540
1541
1542
/**
1543
* Applies configured serialization to a dictionary of values.
1544
*
1545
* @task util
1546
*/
1547
protected function applyLiskDataSerialization(array &$data, $deserialize) {
1548
$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
1549
if ($serialization) {
1550
foreach (array_intersect_key($serialization, $data) as $col => $format) {
1551
switch ($format) {
1552
case self::SERIALIZATION_NONE:
1553
break;
1554
case self::SERIALIZATION_PHP:
1555
if ($deserialize) {
1556
$data[$col] = unserialize($data[$col]);
1557
} else {
1558
$data[$col] = serialize($data[$col]);
1559
}
1560
break;
1561
case self::SERIALIZATION_JSON:
1562
if ($deserialize) {
1563
$data[$col] = json_decode($data[$col], true);
1564
} else {
1565
$data[$col] = phutil_json_encode($data[$col]);
1566
}
1567
break;
1568
default:
1569
throw new Exception(
1570
pht("Unknown serialization format '%s'.", $format));
1571
}
1572
}
1573
}
1574
}
1575
1576
/**
1577
* Black magic. Builds implied get*() and set*() for all properties.
1578
*
1579
* @param string Method name.
1580
* @param list Argument vector.
1581
* @return mixed get*() methods return the property value. set*() methods
1582
* return $this.
1583
* @task util
1584
*/
1585
public function __call($method, $args) {
1586
$dispatch_map = $this->getLiskMetadata('dispatchMap', array());
1587
1588
// NOTE: This method is very performance-sensitive (many thousands of calls
1589
// per page on some pages), and thus has some silliness in the name of
1590
// optimizations.
1591
1592
if ($method[0] === 'g') {
1593
if (isset($dispatch_map[$method])) {
1594
$property = $dispatch_map[$method];
1595
} else {
1596
if (substr($method, 0, 3) !== 'get') {
1597
throw new Exception(pht("Unable to resolve method '%s'!", $method));
1598
}
1599
$property = substr($method, 3);
1600
if (!($property = $this->checkProperty($property))) {
1601
throw new Exception(pht('Bad getter call: %s', $method));
1602
}
1603
$dispatch_map[$method] = $property;
1604
$this->setLiskMetadata('dispatchMap', $dispatch_map);
1605
}
1606
1607
return $this->readField($property);
1608
}
1609
1610
if ($method[0] === 's') {
1611
if (isset($dispatch_map[$method])) {
1612
$property = $dispatch_map[$method];
1613
} else {
1614
if (substr($method, 0, 3) !== 'set') {
1615
throw new Exception(pht("Unable to resolve method '%s'!", $method));
1616
}
1617
1618
$property = substr($method, 3);
1619
$property = $this->checkProperty($property);
1620
if (!$property) {
1621
throw new Exception(pht('Bad setter call: %s', $method));
1622
}
1623
$dispatch_map[$method] = $property;
1624
$this->setLiskMetadata('dispatchMap', $dispatch_map);
1625
}
1626
1627
$this->writeField($property, $args[0]);
1628
1629
return $this;
1630
}
1631
1632
throw new Exception(pht("Unable to resolve method '%s'.", $method));
1633
}
1634
1635
/**
1636
* Warns against writing to undeclared property.
1637
*
1638
* @task util
1639
*/
1640
public function __set($name, $value) {
1641
// Hack for policy system hints, see PhabricatorPolicyRule for notes.
1642
if ($name != '_hashKey') {
1643
phlog(
1644
pht(
1645
'Wrote to undeclared property %s.',
1646
get_class($this).'::$'.$name));
1647
}
1648
$this->$name = $value;
1649
}
1650
1651
1652
/**
1653
* Increments a named counter and returns the next value.
1654
*
1655
* @param AphrontDatabaseConnection Database where the counter resides.
1656
* @param string Counter name to create or increment.
1657
* @return int Next counter value.
1658
*
1659
* @task util
1660
*/
1661
public static function loadNextCounterValue(
1662
AphrontDatabaseConnection $conn_w,
1663
$counter_name) {
1664
1665
// NOTE: If an insert does not touch an autoincrement row or call
1666
// LAST_INSERT_ID(), MySQL normally does not change the value of
1667
// LAST_INSERT_ID(). This can cause a counter's value to leak to a
1668
// new counter if the second counter is created after the first one is
1669
// updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the
1670
// LAST_INSERT_ID() is always updated and always set correctly after the
1671
// query completes.
1672
1673
queryfx(
1674
$conn_w,
1675
'INSERT INTO %T (counterName, counterValue) VALUES
1676
(%s, LAST_INSERT_ID(1))
1677
ON DUPLICATE KEY UPDATE
1678
counterValue = LAST_INSERT_ID(counterValue + 1)',
1679
self::COUNTER_TABLE_NAME,
1680
$counter_name);
1681
1682
return $conn_w->getInsertID();
1683
}
1684
1685
1686
/**
1687
* Returns the current value of a named counter.
1688
*
1689
* @param AphrontDatabaseConnection Database where the counter resides.
1690
* @param string Counter name to read.
1691
* @return int|null Current value, or `null` if the counter does not exist.
1692
*
1693
* @task util
1694
*/
1695
public static function loadCurrentCounterValue(
1696
AphrontDatabaseConnection $conn_r,
1697
$counter_name) {
1698
1699
$row = queryfx_one(
1700
$conn_r,
1701
'SELECT counterValue FROM %T WHERE counterName = %s',
1702
self::COUNTER_TABLE_NAME,
1703
$counter_name);
1704
if (!$row) {
1705
return null;
1706
}
1707
1708
return (int)$row['counterValue'];
1709
}
1710
1711
1712
/**
1713
* Overwrite a named counter, forcing it to a specific value.
1714
*
1715
* If the counter does not exist, it is created.
1716
*
1717
* @param AphrontDatabaseConnection Database where the counter resides.
1718
* @param string Counter name to create or overwrite.
1719
* @return void
1720
*
1721
* @task util
1722
*/
1723
public static function overwriteCounterValue(
1724
AphrontDatabaseConnection $conn_w,
1725
$counter_name,
1726
$counter_value) {
1727
1728
queryfx(
1729
$conn_w,
1730
'INSERT INTO %T (counterName, counterValue) VALUES (%s, %d)
1731
ON DUPLICATE KEY UPDATE counterValue = VALUES(counterValue)',
1732
self::COUNTER_TABLE_NAME,
1733
$counter_name,
1734
$counter_value);
1735
}
1736
1737
private function getBinaryColumns() {
1738
return $this->getConfigOption(self::CONFIG_BINARY);
1739
}
1740
1741
public function getSchemaColumns() {
1742
$custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA);
1743
if (!$custom_map) {
1744
$custom_map = array();
1745
}
1746
1747
$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
1748
if (!$serialization) {
1749
$serialization = array();
1750
}
1751
1752
$serialization_map = array(
1753
self::SERIALIZATION_JSON => 'text',
1754
self::SERIALIZATION_PHP => 'bytes',
1755
);
1756
1757
$binary_map = $this->getBinaryColumns();
1758
1759
$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
1760
if ($id_mechanism == self::IDS_AUTOINCREMENT) {
1761
$id_type = 'auto';
1762
} else {
1763
$id_type = 'id';
1764
}
1765
1766
$builtin = array(
1767
'id' => $id_type,
1768
'phid' => 'phid',
1769
'viewPolicy' => 'policy',
1770
'editPolicy' => 'policy',
1771
'epoch' => 'epoch',
1772
'dateCreated' => 'epoch',
1773
'dateModified' => 'epoch',
1774
);
1775
1776
$map = array();
1777
foreach ($this->getAllLiskProperties() as $property) {
1778
// First, use types specified explicitly in the table configuration.
1779
if (array_key_exists($property, $custom_map)) {
1780
$map[$property] = $custom_map[$property];
1781
continue;
1782
}
1783
1784
// If we don't have an explicit type, try a builtin type for the
1785
// column.
1786
$type = idx($builtin, $property);
1787
if ($type) {
1788
$map[$property] = $type;
1789
continue;
1790
}
1791
1792
// If the column has serialization, we can infer the column type.
1793
if (isset($serialization[$property])) {
1794
$type = idx($serialization_map, $serialization[$property]);
1795
if ($type) {
1796
$map[$property] = $type;
1797
continue;
1798
}
1799
}
1800
1801
if (isset($binary_map[$property])) {
1802
$map[$property] = 'bytes';
1803
continue;
1804
}
1805
1806
if ($property === 'spacePHID') {
1807
$map[$property] = 'phid?';
1808
continue;
1809
}
1810
1811
// If the column is named `somethingPHID`, infer it is a PHID.
1812
if (preg_match('/[a-z]PHID$/', $property)) {
1813
$map[$property] = 'phid';
1814
continue;
1815
}
1816
1817
// If the column is named `somethingID`, infer it is an ID.
1818
if (preg_match('/[a-z]ID$/', $property)) {
1819
$map[$property] = 'id';
1820
continue;
1821
}
1822
1823
// We don't know the type of this column.
1824
$map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN;
1825
}
1826
1827
return $map;
1828
}
1829
1830
public function getSchemaKeys() {
1831
$custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA);
1832
if (!$custom_map) {
1833
$custom_map = array();
1834
}
1835
1836
$default_map = array();
1837
foreach ($this->getAllLiskProperties() as $property) {
1838
switch ($property) {
1839
case 'id':
1840
$default_map['PRIMARY'] = array(
1841
'columns' => array('id'),
1842
'unique' => true,
1843
);
1844
break;
1845
case 'phid':
1846
$default_map['key_phid'] = array(
1847
'columns' => array('phid'),
1848
'unique' => true,
1849
);
1850
break;
1851
case 'spacePHID':
1852
$default_map['key_space'] = array(
1853
'columns' => array('spacePHID'),
1854
);
1855
break;
1856
}
1857
}
1858
1859
return $custom_map + $default_map;
1860
}
1861
1862
public function getColumnMaximumByteLength($column) {
1863
$map = $this->getSchemaColumns();
1864
1865
if (!isset($map[$column])) {
1866
throw new Exception(
1867
pht(
1868
'Object (of class "%s") does not have a column "%s".',
1869
get_class($this),
1870
$column));
1871
}
1872
1873
$data_type = $map[$column];
1874
1875
return id(new PhabricatorStorageSchemaSpec())
1876
->getMaximumByteLengthForDataType($data_type);
1877
}
1878
1879
public function getSchemaPersistence() {
1880
return null;
1881
}
1882
1883
1884
/* -( AphrontDatabaseTableRefInterface )----------------------------------- */
1885
1886
1887
public function getAphrontRefDatabaseName() {
1888
return $this->getDatabaseName();
1889
}
1890
1891
public function getAphrontRefTableName() {
1892
return $this->getTableName();
1893
}
1894
1895
1896
private function getLiskMetadata($key, $default = null) {
1897
if (isset(self::$liskMetadata[static::class][$key])) {
1898
return self::$liskMetadata[static::class][$key];
1899
}
1900
1901
if (!isset(self::$liskMetadata[static::class])) {
1902
self::$liskMetadata[static::class] = array();
1903
}
1904
1905
return idx(self::$liskMetadata[static::class], $key, $default);
1906
}
1907
1908
private function setLiskMetadata($key, $value) {
1909
self::$liskMetadata[static::class][$key] = $value;
1910
}
1911
1912
}
1913
1914